refactor: remove unreachable runner code (#1179)

Remove inherited act APIs, configuration branches, and test seams that neither the daemon nor exec can reach. Constant-fold settings both entry points already enforce and consolidate duplicate runner paths.

Major removals:

- Unwired custom action-cache and local-repository-cache implementations.
- Legacy matrix, platform, input, container-reuse, logging, Git remote, and action-replacement configuration paths.
- Unused Docker socket, container network, tar-copy, and platform PTY wrappers.
- Single-implementation filesystem, environment, runner, and expression abstractions.
- Duplicated step-container, command-logging, credential, reusable-workflow, and execution paths.
- Generated client mock boilerplate, obsolete fixtures, test-only seams, stale wrappers, and commented-out code.

This removes 2844 net Go lines while retaining Gitea RPC, event, matrix, input, cache, artifact, action, reusable workflow, Docker, host, exec, and release behavior.

Assisted by Codex (GPT-5).

Reviewed-on: https://gitea.com/gitea/runner/pulls/1179
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind
2026-08-22 16:54:41 +00:00
committed by silverwind
parent a0c4de79f7
commit 546eca312e
71 changed files with 593 additions and 3507 deletions
+4 -2
View File
@@ -427,8 +427,10 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P
return return
} }
cache := api.ToCache() cache := &Cache{Repo: cred.Repo, Key: api.Key, Version: api.Version, Size: api.Size}
cache.Repo = cred.Repo if cache.Size == 0 {
cache.Size = -1
}
db, err := h.openDB() db, err := h.openDB()
if err != nil { if err != nil {
h.responseJSON(w, r, 500, err) h.responseJSON(w, r, 500, err)
-17
View File
@@ -10,23 +10,6 @@ type Request struct {
Size int64 `json:"cacheSize"` Size int64 `json:"cacheSize"`
} }
func (c *Request) ToCache() *Cache {
if c == nil {
return nil
}
ret := &Cache{
Key: c.Key,
Version: c.Version,
Size: c.Size,
}
if c.Size == 0 {
// So the request comes from old versions of actions, like `actions/cache@v2`.
// It doesn't send cache size. Set it to -1 to indicate that.
ret.Size = -1
}
return ret
}
type Cache struct { type Cache struct {
ID uint64 `json:"id" boltholdKey:"ID"` ID uint64 `json:"id" boltholdKey:"ID"`
Repo string `json:"repo" boltholdIndex:"Repo"` Repo string `json:"repo" boltholdIndex:"Repo"`
+33 -105
View File
@@ -50,65 +50,29 @@ type ResponseMessage struct {
Message string `json:"message"` Message string `json:"message"`
} }
type WritableFile interface {
io.WriteCloser
}
type WriteFS interface {
OpenWritable(name string) (WritableFile, error)
OpenAppendable(name string) (WritableFile, error)
}
type readWriteFSImpl struct{}
func (fwfs readWriteFSImpl) Open(name string) (fs.File, error) {
return os.Open(name)
}
func (fwfs readWriteFSImpl) OpenWritable(name string) (WritableFile, error) {
if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
return nil, err
}
return os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644)
}
func (fwfs readWriteFSImpl) OpenAppendable(name string) (WritableFile, error) {
if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
return nil, err
}
file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0o644)
if err != nil {
return nil, err
}
_, err = file.Seek(0, io.SeekEnd)
if err != nil {
return nil, err
}
return file, nil
}
var gzipExtension = ".gz__" var gzipExtension = ".gz__"
func safeResolve(baseDir, relPath string) string { func safeResolve(baseDir, relPath string) string {
return filepath.Join(baseDir, filepath.Clean(filepath.Join(string(os.PathSeparator), relPath))) return filepath.Join(baseDir, filepath.Clean(filepath.Join(string(os.PathSeparator), relPath)))
} }
func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) { func writeJSON(w http.ResponseWriter, value any) {
data, err := json.Marshal(value)
if err != nil {
panic(err)
}
if _, err := w.Write(data); err != nil {
panic(err)
}
}
func uploads(router *httprouter.Router, baseDir string) {
router.POST("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { router.POST("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
runID := params.ByName("runId") runID := params.ByName("runId")
json, err := json.Marshal(FileContainerResourceURL{ writeJSON(w, FileContainerResourceURL{
FileContainerResourceURL: fmt.Sprintf("http://%s/upload/%s", req.Host, runID), FileContainerResourceURL: fmt.Sprintf("http://%s/upload/%s", req.Host, runID),
}) })
if err != nil {
panic(err)
}
_, err = w.Write(json)
if err != nil {
panic(err)
}
}) })
router.PUT("/upload/:runId", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { router.PUT("/upload/:runId", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
@@ -122,67 +86,47 @@ func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) {
safeRunPath := safeResolve(baseDir, runID) safeRunPath := safeResolve(baseDir, runID)
safePath := safeResolve(safeRunPath, itemPath) safePath := safeResolve(safeRunPath, itemPath)
file, err := func() (WritableFile, error) { if err := os.MkdirAll(filepath.Dir(safePath), os.ModePerm); err != nil {
contentRange := req.Header.Get("Content-Range") panic(err)
if contentRange != "" && !strings.HasPrefix(contentRange, "bytes 0-") {
return fsys.OpenAppendable(safePath)
} }
return fsys.OpenWritable(safePath) flags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
}() appendUpload := req.Header.Get("Content-Range")
if appendUpload != "" && !strings.HasPrefix(appendUpload, "bytes 0-") {
flags = os.O_CREATE | os.O_WRONLY | os.O_APPEND
}
file, err := os.OpenFile(safePath, flags, 0o644)
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer file.Close() defer file.Close()
writer, ok := file.(io.Writer)
if !ok {
panic(errors.New("File is not writable"))
}
if req.Body == nil { if req.Body == nil {
panic(errors.New("No body given")) panic(errors.New("No body given"))
} }
_, err = io.Copy(writer, req.Body) _, err = io.Copy(file, req.Body)
if err != nil { if err != nil {
panic(err) panic(err)
} }
json, err := json.Marshal(ResponseMessage{ writeJSON(w, ResponseMessage{
Message: "success", Message: "success",
}) })
if err != nil {
panic(err)
}
_, err = w.Write(json)
if err != nil {
panic(err)
}
}) })
router.PATCH("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { router.PATCH("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
json, err := json.Marshal(ResponseMessage{ writeJSON(w, ResponseMessage{
Message: "success", Message: "success",
}) })
if err != nil {
panic(err)
}
_, err = w.Write(json)
if err != nil {
panic(err)
}
}) })
} }
func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) { func downloads(router *httprouter.Router, baseDir string) {
router.GET("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { router.GET("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
runID := params.ByName("runId") runID := params.ByName("runId")
safePath := safeResolve(baseDir, runID) safePath := safeResolve(baseDir, runID)
entries, err := fs.ReadDir(fsys, safePath) entries, err := os.ReadDir(safePath)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -195,18 +139,10 @@ func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
}) })
} }
json, err := json.Marshal(NamedFileContainerResourceURLResponse{ writeJSON(w, NamedFileContainerResourceURLResponse{
Count: len(list), Count: len(list),
Value: list, Value: list,
}) })
if err != nil {
panic(err)
}
_, err = w.Write(json)
if err != nil {
panic(err)
}
}) })
router.GET("/download/:container", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { router.GET("/download/:container", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
@@ -215,7 +151,7 @@ func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
safePath := safeResolve(baseDir, filepath.Join(container, itemPath)) safePath := safeResolve(baseDir, filepath.Join(container, itemPath))
var files []ContainerItem var files []ContainerItem
err := fs.WalkDir(fsys, safePath, func(path string, entry fs.DirEntry, err error) error { err := filepath.WalkDir(safePath, func(path string, entry fs.DirEntry, err error) error {
if !entry.IsDir() { if !entry.IsDir() {
rel, err := filepath.Rel(safePath, path) rel, err := filepath.Rel(safePath, path)
if err != nil { if err != nil {
@@ -241,17 +177,9 @@ func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
panic(err) panic(err)
} }
json, err := json.Marshal(ContainerItemResponse{ writeJSON(w, ContainerItemResponse{
Value: files, Value: files,
}) })
if err != nil {
panic(err)
}
_, err = w.Write(json)
if err != nil {
panic(err)
}
}) })
router.GET("/artifact/*path", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { router.GET("/artifact/*path", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
@@ -259,15 +187,16 @@ func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) {
safePath := safeResolve(baseDir, path) safePath := safeResolve(baseDir, path)
file, err := fsys.Open(safePath) file, err := os.Open(safePath)
if err != nil { if err != nil {
// try gzip file // try gzip file
file, err = fsys.Open(safePath + gzipExtension) file, err = os.Open(safePath + gzipExtension)
if err != nil { if err != nil {
panic(err) panic(err)
} }
w.Header().Add("Content-Encoding", "gzip") w.Header().Add("Content-Encoding", "gzip")
} }
defer file.Close()
_, err = io.Copy(w, file) _, err = io.Copy(w, file)
if err != nil { if err != nil {
@@ -287,9 +216,8 @@ func Serve(ctx context.Context, artifactPath, addr, port string) context.CancelF
router := httprouter.New() router := httprouter.New()
logger.Debugf("Artifacts base path '%s'", artifactPath) logger.Debugf("Artifacts base path '%s'", artifactPath)
fsys := readWriteFSImpl{} uploads(router, artifactPath)
uploads(router, artifactPath, fsys) downloads(router, artifactPath)
downloads(router, artifactPath, fsys)
server := &http.Server{ server := &http.Server{
Addr: fmt.Sprintf("%s:%s", addr, port), Addr: fmt.Sprintf("%s:%s", addr, port),
+21 -313
View File
@@ -8,7 +8,6 @@ import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"maps" "maps"
"net/http" "net/http"
@@ -18,238 +17,18 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"testing/fstest"
"time" "time"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type writableMapFile struct {
fstest.MapFile
}
func (f *writableMapFile) Write(data []byte) (int, error) {
f.Data = data
return len(data), nil
}
func (f *writableMapFile) Close() error {
return nil
}
type writeMapFS struct {
fstest.MapFS
}
func (fsys writeMapFS) OpenWritable(name string) (WritableFile, error) {
file := &writableMapFile{
MapFile: fstest.MapFile{
Data: []byte("content2"),
},
}
fsys.MapFS[name] = &file.MapFile
return file, nil
}
func (fsys writeMapFS) OpenAppendable(name string) (WritableFile, error) {
file := &writableMapFile{
MapFile: fstest.MapFile{
Data: []byte("content2"),
},
}
fsys.MapFS[name] = &file.MapFile
return file, nil
}
func TestNewArtifactUploadPrepare(t *testing.T) {
assert := assert.New(t)
memfs := fstest.MapFS(map[string]*fstest.MapFile{})
router := httprouter.New()
uploads(router, "artifact/server/path", writeMapFS{memfs})
req, _ := http.NewRequest(http.MethodPost, "http://localhost/_apis/pipelines/workflows/1/artifacts", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
assert.Fail("Wrong status")
}
response := FileContainerResourceURL{}
err := json.Unmarshal(rr.Body.Bytes(), &response)
if err != nil {
panic(err)
}
assert.Equal("http://localhost/upload/1", response.FileContainerResourceURL)
}
func TestArtifactUploadBlob(t *testing.T) {
assert := assert.New(t)
memfs := fstest.MapFS(map[string]*fstest.MapFile{})
router := httprouter.New()
uploads(router, "artifact/server/path", writeMapFS{memfs})
req, _ := http.NewRequest(http.MethodPut, "http://localhost/upload/1?itemPath=some/file", strings.NewReader("content"))
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
assert.Fail("Wrong status")
}
response := ResponseMessage{}
err := json.Unmarshal(rr.Body.Bytes(), &response)
if err != nil {
panic(err)
}
assert.Equal("success", response.Message)
assert.Equal("content", string(memfs["artifact/server/path/1/some/file"].Data))
}
func TestFinalizeArtifactUpload(t *testing.T) {
assert := assert.New(t)
memfs := fstest.MapFS(map[string]*fstest.MapFile{})
router := httprouter.New()
uploads(router, "artifact/server/path", writeMapFS{memfs})
req, _ := http.NewRequest(http.MethodPatch, "http://localhost/_apis/pipelines/workflows/1/artifacts", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
assert.Fail("Wrong status")
}
response := ResponseMessage{}
err := json.Unmarshal(rr.Body.Bytes(), &response)
if err != nil {
panic(err)
}
assert.Equal("success", response.Message)
}
func TestListArtifacts(t *testing.T) {
assert := assert.New(t)
memfs := fstest.MapFS(map[string]*fstest.MapFile{
"artifact/server/path/1/file.txt": {
Data: []byte(""),
},
})
router := httprouter.New()
downloads(router, "artifact/server/path", memfs)
req, _ := http.NewRequest(http.MethodGet, "http://localhost/_apis/pipelines/workflows/1/artifacts", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
assert.FailNow(fmt.Sprintf("Wrong status: %d", status))
}
response := NamedFileContainerResourceURLResponse{}
err := json.Unmarshal(rr.Body.Bytes(), &response)
if err != nil {
panic(err)
}
assert.Equal(1, response.Count)
assert.Equal("file.txt", response.Value[0].Name)
assert.Equal("http://localhost/download/1", response.Value[0].FileContainerResourceURL)
}
func TestListArtifactContainer(t *testing.T) {
assert := assert.New(t)
memfs := fstest.MapFS(map[string]*fstest.MapFile{
"artifact/server/path/1/some/file": {
Data: []byte(""),
},
})
router := httprouter.New()
downloads(router, "artifact/server/path", memfs)
req, _ := http.NewRequest(http.MethodGet, "http://localhost/download/1?itemPath=some/file", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
assert.FailNow(fmt.Sprintf("Wrong status: %d", status))
}
response := ContainerItemResponse{}
err := json.Unmarshal(rr.Body.Bytes(), &response)
if err != nil {
panic(err)
}
assert.Len(response.Value, 1)
assert.Equal("some/file", response.Value[0].Path)
assert.Equal("file", response.Value[0].ItemType)
assert.Equal("http://localhost/artifact/1/some/file/.", response.Value[0].ContentLocation)
}
func TestDownloadArtifactFile(t *testing.T) {
assert := assert.New(t)
memfs := fstest.MapFS(map[string]*fstest.MapFile{
"artifact/server/path/1/some/file": {
Data: []byte("content"),
},
})
router := httprouter.New()
downloads(router, "artifact/server/path", memfs)
req, _ := http.NewRequest(http.MethodGet, "http://localhost/artifact/1/some/file", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
assert.FailNow(fmt.Sprintf("Wrong status: %d", status))
}
data := rr.Body.Bytes()
assert.Equal("content", string(data))
}
// TestArtifactFlow drives the real Serve() artifact server over a loopback socket, exercising
// the same upload -> finalize -> list -> download protocol the upload-artifact/download-artifact
// actions speak. Running it in-process (rather than from a job container) keeps it network-free
// and reachable everywhere, including when the CI job is itself a container.
func TestArtifactFlow(t *testing.T) { func TestArtifactFlow(t *testing.T) {
artifactPath := t.TempDir() artifactPath := t.TempDir()
// Serve the exact routes Serve() wires up, on a real loopback socket via httptest. httptest
// picks a free port and Close() tears the server down synchronously — avoiding both the
// port-rebind race and Serve()'s detached ListenAndServe goroutine, which logger.Fatal()s
// (process exit) on a bind error and can outlive the test's temp-dir cleanup.
router := httprouter.New() router := httprouter.New()
fsys := readWriteFSImpl{} uploads(router, artifactPath)
uploads(router, artifactPath, fsys) downloads(router, artifactPath)
downloads(router, artifactPath, fsys)
server := httptest.NewServer(router) server := httptest.NewServer(router)
defer server.Close() defer server.Close()
@@ -257,8 +36,6 @@ func TestArtifactFlow(t *testing.T) {
client := server.Client() client := server.Client()
client.Timeout = 5 * time.Second client.Timeout = 5 * time.Second
// request performs one HTTP call and returns the status and body. The default transport adds
// Accept-Encoding: gzip and transparently decompresses, so gzipped downloads come back plain.
request := func(t *testing.T, method, rawURL string, body io.Reader, header http.Header) (int, []byte) { request := func(t *testing.T, method, rawURL string, body io.Reader, header http.Header) (int, []byte) {
t.Helper() t.Helper()
req, err := http.NewRequest(method, rawURL, body) req, err := http.NewRequest(method, rawURL, body)
@@ -289,6 +66,8 @@ func TestArtifactFlow(t *testing.T) {
status, data = request(t, http.MethodPatch, baseURL+"/_apis/pipelines/workflows/"+runID+"/artifacts", nil, nil) status, data = request(t, http.MethodPatch, baseURL+"/_apis/pipelines/workflows/"+runID+"/artifacts", nil, nil)
require.Equal(t, http.StatusOK, status, string(data)) require.Equal(t, http.StatusOK, status, string(data))
require.NoError(t, json.Unmarshal(data, &msg))
require.Equal(t, "success", msg.Message)
status, data = request(t, http.MethodGet, baseURL+"/_apis/pipelines/workflows/"+runID+"/artifacts", nil, nil) status, data = request(t, http.MethodGet, baseURL+"/_apis/pipelines/workflows/"+runID+"/artifacts", nil, nil)
require.Equal(t, http.StatusOK, status, string(data)) require.Equal(t, http.StatusOK, status, string(data))
@@ -314,6 +93,21 @@ func TestArtifactFlow(t *testing.T) {
require.Equal(t, content, string(stored)) require.Equal(t, content, string(stored))
}) })
t.Run("content-range", func(t *testing.T) {
const rawURL = "/upload/4?itemPath=chunks.txt"
status, data := request(t, http.MethodPut, baseURL+rawURL, strings.NewReader("first"),
http.Header{"Content-Range": []string{"bytes 0-4/11"}})
require.Equal(t, http.StatusOK, status, string(data))
status, data = request(t, http.MethodPut, baseURL+rawURL, strings.NewReader("-second"),
http.Header{"Content-Range": []string{"bytes 5-11/11"}})
require.Equal(t, http.StatusOK, status, string(data))
stored, err := os.ReadFile(filepath.Join(artifactPath, "4", "chunks.txt"))
require.NoError(t, err)
require.Equal(t, "first-second", string(stored))
})
t.Run("gzip-roundtrip", func(t *testing.T) { t.Run("gzip-roundtrip", func(t *testing.T) {
const runID, item, content = "2", "logs/app.log", "compressed payload\n" const runID, item, content = "2", "logs/app.log", "compressed payload\n"
@@ -365,9 +159,7 @@ func TestArtifactFlow(t *testing.T) {
}) })
} }
func TestMkdirFsImplSafeResolve(t *testing.T) { func TestSafeResolve(t *testing.T) {
assert := assert.New(t)
baseDir := "/foo/bar" baseDir := "/foo/bar"
tests := map[string]struct { tests := map[string]struct {
@@ -385,97 +177,13 @@ func TestMkdirFsImplSafeResolve(t *testing.T) {
for name, tc := range tests { for name, tc := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert.Equal(tc.want, safeResolve(baseDir, tc.input)) require.Equal(t, tc.want, safeResolve(baseDir, tc.input))
}) })
} }
} }
func TestReadWriteFSWritableAndAppendable(t *testing.T) {
fsys := readWriteFSImpl{}
name := filepath.Join(t.TempDir(), "nested", "artifact.txt")
w, err := fsys.OpenWritable(name)
require.NoError(t, err)
_, err = w.Write([]byte("first"))
require.NoError(t, err)
require.NoError(t, w.Close())
w, err = fsys.OpenAppendable(name)
require.NoError(t, err)
_, err = w.Write([]byte("-second"))
require.NoError(t, err)
require.NoError(t, w.Close())
got, err := os.ReadFile(name)
require.NoError(t, err)
require.Equal(t, "first-second", string(got))
w, err = fsys.OpenWritable(name)
require.NoError(t, err)
_, err = w.Write([]byte("replaced"))
require.NoError(t, err)
require.NoError(t, w.Close())
got, err = os.ReadFile(name)
require.NoError(t, err)
require.Equal(t, "replaced", string(got))
}
func TestServeEmptyArtifactPathReturnsCancelableNoop(t *testing.T) { func TestServeEmptyArtifactPathReturnsCancelableNoop(t *testing.T) {
cancel := Serve(t.Context(), "", "127.0.0.1", "0") cancel := Serve(t.Context(), "", "127.0.0.1", "0")
require.NotNil(t, cancel) require.NotNil(t, cancel)
cancel() cancel()
} }
func TestDownloadArtifactFileUnsafePath(t *testing.T) {
assert := assert.New(t)
memfs := fstest.MapFS(map[string]*fstest.MapFile{
"artifact/server/path/some/file": {
Data: []byte("content"),
},
})
router := httprouter.New()
downloads(router, "artifact/server/path", memfs)
req, _ := http.NewRequest(http.MethodGet, "http://localhost/artifact/2/../../some/file", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
assert.FailNow(fmt.Sprintf("Wrong status: %d", status))
}
data := rr.Body.Bytes()
assert.Equal("content", string(data))
}
func TestArtifactUploadBlobUnsafePath(t *testing.T) {
assert := assert.New(t)
memfs := fstest.MapFS(map[string]*fstest.MapFile{})
router := httprouter.New()
uploads(router, "artifact/server/path", writeMapFS{memfs})
req, _ := http.NewRequest(http.MethodPut, "http://localhost/upload/1?itemPath=../../some/file", strings.NewReader("content"))
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
assert.Fail("Wrong status")
}
response := ResponseMessage{}
err := json.Unmarshal(rr.Body.Bytes(), &response)
if err != nil {
panic(err)
}
assert.Equal("success", response.Message)
assert.Equal("content", string(memfs["artifact/server/path/1/some/file"].Data))
}
-23
View File
@@ -54,22 +54,6 @@ func NewPipelineExecutor(executors ...Executor) Executor {
return rtn return rtn
} }
// NewConditionalExecutor creates a new executor based on conditions
func NewConditionalExecutor(conditional Conditional, trueExecutor, falseExecutor Executor) Executor {
return func(ctx context.Context) error {
if conditional(ctx) {
if trueExecutor != nil {
return trueExecutor(ctx)
}
} else {
if falseExecutor != nil {
return falseExecutor(ctx)
}
}
return nil
}
}
// NewErrorExecutor creates a new executor that always errors out // NewErrorExecutor creates a new executor that always errors out
func NewErrorExecutor(err error) Executor { func NewErrorExecutor(err error) Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
@@ -192,10 +176,3 @@ func (e Executor) Finally(finally Executor) Executor {
return err return err
} }
} }
// Not return an inverted conditional
func (c Conditional) Not() Conditional {
return func(ctx context.Context) bool {
return !c(ctx)
}
}
-44
View File
@@ -45,43 +45,6 @@ func TestNewWorkflow(t *testing.T) {
assert.Equal(2, runcount) assert.Equal(2, runcount)
} }
func TestNewConditionalExecutor(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
trueCount := 0
falseCount := 0
err := NewConditionalExecutor(func(ctx context.Context) bool {
return false
}, func(ctx context.Context) error {
trueCount++
return nil
}, func(ctx context.Context) error {
falseCount++
return nil
})(ctx)
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(0, trueCount)
assert.Equal(1, falseCount)
err = NewConditionalExecutor(func(ctx context.Context) bool {
return true
}, func(ctx context.Context) error {
trueCount++
return nil
}, func(ctx context.Context) error {
falseCount++
return nil
})(ctx)
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(1, trueCount)
assert.Equal(1, falseCount)
}
// concurrencyProbe returns an executor recording the peak number of concurrent copies. Copies // concurrencyProbe returns an executor recording the peak number of concurrent copies. Copies
// block until wantActive are in flight so the peak is exact without sleeping, and later copies // block until wantActive are in flight so the peak is exact without sleeping, and later copies
// find the gate already open so the last one still finishes with no partner left. // find the gate already open so the last one still finishes with no partner left.
@@ -223,10 +186,3 @@ func TestExecutorFinallyReturnsFinallyErrorWithOriginal(t *testing.T) {
t.Fatalf("finally error = %q, want both cleanup and original error", err) t.Fatalf("finally error = %q, want both cleanup and original error", err)
} }
} }
func TestConditionalNot(t *testing.T) {
cond := Conditional(func(context.Context) bool { return false })
if !cond.Not()(context.Background()) {
t.Fatal("inverted conditional should be true")
}
}
+12 -16
View File
@@ -36,7 +36,6 @@ var (
cloneLocks lock.Keyed[string] // key: clone target directory cloneLocks lock.Keyed[string] // key: clone target directory
ErrShortRef = errors.New("short SHA references are not supported") ErrShortRef = errors.New("short SHA references are not supported")
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.
@@ -187,19 +186,16 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
} }
// FindGithubRepo get the repo // FindGithubRepo get the repo
func FindGithubRepo(ctx context.Context, file, githubInstance, remoteName string) (string, error) { func FindGithubRepo(ctx context.Context, file, githubInstance string) (string, error) {
goGitMu.Lock() goGitMu.Lock()
defer goGitMu.Unlock() defer goGitMu.Unlock()
if remoteName == "" {
remoteName = "origin"
}
url, err := findGitRemoteURL(ctx, file, remoteName) url, err := findGitRemoteURL(ctx, file, "origin")
if err != nil { if err != nil {
return "", err return "", err
} }
_, slug, err := findGitSlug(url, githubInstance) _, slug := findGitSlug(url, githubInstance)
return slug, err return slug, nil
} }
func findGitRemoteURL(_ context.Context, file, remoteName string) (string, error) { func findGitRemoteURL(_ context.Context, file, remoteName string) (string, error) {
@@ -226,25 +222,25 @@ func findGitRemoteURL(_ context.Context, file, remoteName string) (string, error
return remote.Config().URLs[0], nil return remote.Config().URLs[0], nil
} }
func findGitSlug(url, githubInstance string) (string, string, error) { //nolint:unparam // pre-existing issue from nektos/act func findGitSlug(url, githubInstance string) (string, string) {
if matches := codeCommitHTTPRegex.FindStringSubmatch(url); matches != nil { if matches := codeCommitHTTPRegex.FindStringSubmatch(url); matches != nil {
return "CodeCommit", matches[2], nil return "CodeCommit", matches[2]
} else if matches := codeCommitSSHRegex.FindStringSubmatch(url); matches != nil { } else if matches := codeCommitSSHRegex.FindStringSubmatch(url); matches != nil {
return "CodeCommit", matches[2], nil return "CodeCommit", matches[2]
} else if matches := githubHTTPRegex.FindStringSubmatch(url); matches != nil { } else if matches := githubHTTPRegex.FindStringSubmatch(url); matches != nil {
return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2])
} else if matches := githubSSHRegex.FindStringSubmatch(url); matches != nil { } else if matches := githubSSHRegex.FindStringSubmatch(url); matches != nil {
return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2])
} else if githubInstance != "github.com" { } else if githubInstance != "github.com" {
gheHTTPRegex := regexp.MustCompile(fmt.Sprintf(`^https?://%s/(.+)/(.+?)(?:.git)?$`, githubInstance)) gheHTTPRegex := regexp.MustCompile(fmt.Sprintf(`^https?://%s/(.+)/(.+?)(?:.git)?$`, githubInstance))
gheSSHRegex := regexp.MustCompile(githubInstance + "[:/](.+)/(.+?)(?:.git)?$") gheSSHRegex := regexp.MustCompile(githubInstance + "[:/](.+)/(.+?)(?:.git)?$")
if matches := gheHTTPRegex.FindStringSubmatch(url); matches != nil { if matches := gheHTTPRegex.FindStringSubmatch(url); matches != nil {
return "GitHubEnterprise", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil return "GitHubEnterprise", fmt.Sprintf("%s/%s", matches[1], matches[2])
} else if matches := gheSSHRegex.FindStringSubmatch(url); matches != nil { } else if matches := gheSSHRegex.FindStringSubmatch(url); matches != nil {
return "GitHubEnterprise", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil return "GitHubEnterprise", fmt.Sprintf("%s/%s", matches[1], matches[2])
} }
} }
return "", url, nil return "", url
} }
// NewGitCloneExecutorInput the input for the NewGitCloneExecutor // NewGitCloneExecutorInput the input for the NewGitCloneExecutor
+9 -36
View File
@@ -51,9 +51,7 @@ func TestFindGitSlug(t *testing.T) {
} }
for _, tt := range slugTests { for _, tt := range slugTests {
provider, slug, err := findGitSlug(tt.url, "github.com") provider, slug := findGitSlug(tt.url, "github.com")
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(tt.provider, provider) assert.Equal(tt.provider, provider)
assert.Equal(tt.slug, slug) assert.Equal(tt.slug, slug)
} }
@@ -87,45 +85,20 @@ func cleanGitHooks(dir string) error {
return nil return nil
} }
func TestFindGitRemoteURL(t *testing.T) { func TestFindGithubRepoUsesOrigin(t *testing.T) {
assert := assert.New(t)
basedir := t.TempDir()
err := gitCmd("init", basedir)
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
err = cleanGitHooks(basedir)
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
remoteURL := "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo-name"
err = gitCmd("-C", basedir, "remote", "add", "origin", remoteURL)
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
u, err := findGitRemoteURL(context.Background(), basedir, "origin")
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(remoteURL, u)
remoteURL = "git@github.com/AwesomeOwner/MyAwesomeRepo.git"
err = gitCmd("-C", basedir, "remote", "add", "upstream", remoteURL)
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
u, err = findGitRemoteURL(context.Background(), basedir, "upstream")
assert.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(remoteURL, u)
}
func TestFindGithubRepoUsesOriginAndCustomRemote(t *testing.T) {
basedir := t.TempDir() basedir := t.TempDir()
const remoteURL = "https://github.com/owner/repo.git"
require.NoError(t, gitCmd("init", basedir)) require.NoError(t, gitCmd("init", basedir))
require.NoError(t, cleanGitHooks(basedir)) require.NoError(t, cleanGitHooks(basedir))
require.NoError(t, gitCmd("-C", basedir, "remote", "add", "origin", "https://github.com/owner/repo.git")) require.NoError(t, gitCmd("-C", basedir, "remote", "add", "origin", remoteURL))
require.NoError(t, gitCmd("-C", basedir, "remote", "add", "ghe", "git@git.example.com:team/project.git"))
slug, err := FindGithubRepo(context.Background(), basedir, "github.com", "") url, err := findGitRemoteURL(context.Background(), basedir, "origin")
require.NoError(t, err)
require.Equal(t, remoteURL, url)
slug, err := FindGithubRepo(context.Background(), basedir, "github.com")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "owner/repo", slug) require.Equal(t, "owner/repo", slug)
slug, err = FindGithubRepo(context.Background(), basedir, "git.example.com", "ghe")
require.NoError(t, err)
require.Equal(t, "team/project", slug)
} }
func TestGitFindRef(t *testing.T) { func TestGitFindRef(t *testing.T) {
-2
View File
@@ -88,9 +88,7 @@ type Info struct {
// Container for managing docker run containers // Container for managing docker run containers
type Container interface { type Container interface {
Create(capAdd, capDrop []string) common.Executor Create(capAdd, capDrop []string) common.Executor
ConnectToNetwork(name string) common.Executor
Copy(destPath string, files ...*FileEntry) common.Executor Copy(destPath string, files ...*FileEntry) common.Executor
CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error
CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor
GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error)
Inspect(ctx context.Context) (*Info, error) Inspect(ctx context.Context) (*Info, error)
-60
View File
@@ -65,29 +65,6 @@ func NewContainer(input *NewContainerInput) ExecutionsEnvironment {
return cr return cr
} }
func (cr *containerReference) ConnectToNetwork(name string) common.Executor {
return common.
NewDebugExecutor("docker network connect %s %s", name, cr.input.Name).
Then(
common.NewPipelineExecutor(
cr.connect(),
cr.connectToNetwork(name, cr.input.NetworkAliases),
).IfNot(common.Dryrun),
)
}
func (cr *containerReference) connectToNetwork(name string, aliases []string) common.Executor {
return func(ctx context.Context) error {
_, err := cr.cli.NetworkConnect(ctx, name, client.NetworkConnectOptions{
Container: cr.input.Name,
EndpointConfig: &network.EndpointSettings{
Aliases: aliases,
},
})
return err
}
}
// supportsContainerImagePlatform reports whether the Docker server API version // supportsContainerImagePlatform reports whether the Docker server API version
// is 1.41 and beyond // is 1.41 and beyond
func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) (bool, error) { func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) (bool, error) {
@@ -944,42 +921,6 @@ func (cr *containerReference) waitForCommand(ctx context.Context, resp client.Hi
} }
} }
func (cr *containerReference) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error {
if cr.id == "" {
return cr.missingContainerError("copy to %s", destPath)
}
// Mkdir, with a path relative to the DestinationPath ("/") below. Docker 29.5+
// rejects absolute tar entry names with "path escapes from parent".
buf := &bytes.Buffer{}
tw := tar.NewWriter(buf)
_ = tw.WriteHeader(&tar.Header{
Name: strings.TrimPrefix(destPath, "/"),
Mode: 0o777,
Typeflag: tar.TypeDir,
})
tw.Close()
_, err := cr.cli.CopyToContainer(ctx, cr.id, client.CopyToContainerOptions{
DestinationPath: "/",
Content: buf,
})
if err != nil {
return fmt.Errorf("failed to mkdir to copy content to container: %w", err)
}
// Copy Content
_, err = cr.cli.CopyToContainer(ctx, cr.id, client.CopyToContainerOptions{
DestinationPath: destPath,
Content: tarStream,
})
if err != nil {
return fmt.Errorf("failed to copy content to container: %w", err)
}
// If this fails, then folders have wrong permissions on non root container
if cr.UID != 0 || cr.GID != 0 {
_ = cr.Exec([]string{"chown", "-R", fmt.Sprintf("%d:%d", cr.UID, cr.GID), destPath}, nil, "0", "")(ctx)
}
return nil
}
func (cr *containerReference) copyDir(dstPath, srcPath string, useGitIgnore bool) common.Executor { func (cr *containerReference) copyDir(dstPath, srcPath string, useGitIgnore bool) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
if cr.id == "" { if cr.id == "" {
@@ -1021,7 +962,6 @@ func (cr *containerReference) copyDir(dstPath, srcPath string, useGitIgnore bool
} }
fc := &filecollector.FileCollector{ fc := &filecollector.FileCollector{
Fs: &filecollector.DefaultFs{},
Ignorer: ignorer, Ignorer: ignorer,
SrcPath: srcPath, SrcPath: srcPath,
SrcPrefix: srcPrefix, SrcPrefix: srcPrefix,
-141
View File
@@ -5,7 +5,6 @@
package container package container
import ( import (
"archive/tar"
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
@@ -342,116 +341,6 @@ func TestDockerWaitFailure(t *testing.T) {
client.AssertExpectations(t) client.AssertExpectations(t)
} }
func TestDockerCopyTarStream(t *testing.T) {
ctx := context.Background()
client := &mockDockerClient{}
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
return opts.DestinationPath == "/" && opts.Content != nil
})).Return(mobyclient.CopyToContainerResult{}, nil)
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
return opts.DestinationPath == "/var/run/act" && opts.Content != nil
})).Return(mobyclient.CopyToContainerResult{}, nil)
cr := &containerReference{
id: "123",
cli: client,
input: &NewContainerInput{
Image: "image",
},
}
_ = cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{})
client.AssertExpectations(t)
}
// Docker 29.5+ rejects absolute names in the mkdir tarball with
// "path escapes from parent", since it is extracted relative to "/".
func TestDockerCopyTarStreamMkdirEntryIsRelative(t *testing.T) {
ctx := context.Background()
var mkdirNames []string
client := &mockDockerClient{}
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
if opts.DestinationPath != "/" || opts.Content == nil {
return false
}
tr := tar.NewReader(opts.Content)
for {
hdr, err := tr.Next()
if err != nil {
break
}
mkdirNames = append(mkdirNames, hdr.Name)
}
return true
})).Return(mobyclient.CopyToContainerResult{}, nil)
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
return opts.DestinationPath == "/var/run/act" && opts.Content != nil
})).Return(mobyclient.CopyToContainerResult{}, nil)
cr := &containerReference{
id: "123",
cli: client,
input: &NewContainerInput{
Image: "image",
},
}
require.NoError(t, cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{}))
assert.Equal(t, []string{"var/run/act"}, mkdirNames)
client.AssertExpectations(t)
}
func TestDockerCopyTarStreamErrorInCopyFiles(t *testing.T) {
ctx := context.Background()
merr := errors.New("Failure")
client := &mockDockerClient{}
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
return opts.DestinationPath == "/" && opts.Content != nil
})).Return(mobyclient.CopyToContainerResult{}, merr)
cr := &containerReference{
id: "123",
cli: client,
input: &NewContainerInput{
Image: "image",
},
}
err := cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{})
assert.ErrorIs(t, err, merr) //nolint:testifylint // pre-existing issue from nektos/act
client.AssertExpectations(t)
}
func TestDockerCopyTarStreamErrorInMkdir(t *testing.T) {
ctx := context.Background()
merr := errors.New("Failure")
client := &mockDockerClient{}
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
return opts.DestinationPath == "/" && opts.Content != nil
})).Return(mobyclient.CopyToContainerResult{}, nil)
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
return opts.DestinationPath == "/var/run/act" && opts.Content != nil
})).Return(mobyclient.CopyToContainerResult{}, merr)
cr := &containerReference{
id: "123",
cli: client,
input: &NewContainerInput{
Image: "image",
},
}
err := cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{})
assert.ErrorIs(t, err, merr) //nolint:testifylint // pre-existing issue from nektos/act
client.AssertExpectations(t)
}
// A remove that raced the daemon's AutoRemove teardown is not a failure and must not // A remove that raced the daemon's AutoRemove teardown is not a failure and must not
// be logged as one. // be logged as one.
func TestRemoveIgnoresAutoRemoveRace(t *testing.T) { func TestRemoveIgnoresAutoRemoveRace(t *testing.T) {
@@ -582,7 +471,6 @@ func TestRejectsMissingContainer(t *testing.T) {
} }
check("copyContent", cr.copyContent("/var/run/act", &FileEntry{Name: "x", Mode: 0o644})(ctx)) check("copyContent", cr.copyContent("/var/run/act", &FileEntry{Name: "x", Mode: 0o644})(ctx))
check("copyDir", cr.copyDir("/var/run/act", "/src", false)(ctx)) check("copyDir", cr.copyDir("/var/run/act", "/src", false)(ctx))
check("CopyTarStream", cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{}))
check("exec", cr.exec([]string{"echo"}, nil, "", "")(ctx)) check("exec", cr.exec([]string{"echo"}, nil, "", "")(ctx))
_, err := cr.GetContainerArchive(ctx, "/var/run/act/x") _, err := cr.GetContainerArchive(ctx, "/var/run/act/x")
check("GetContainerArchive", err) check("GetContainerArchive", err)
@@ -618,35 +506,6 @@ func TestPublicCopyPipelineHandlesStaleID(t *testing.T) {
client.AssertExpectations(t) client.AssertExpectations(t)
} }
// TestDockerCopyToSymlinkPath is a regression test for gitea/runner#981. Most base images
// symlink /var/run to /run, so copying into /var/run/act traverses that symlink. The broken
// docker 29.5.1 daemon fails the extraction with "mkdirat var/run: file exists" (fixed in
// 29.5.2). Running against the daemon shipped in the dind image, this catches a bad bump.
func TestDockerCopyToSymlinkPath(t *testing.T) {
requireDocker(t)
ctx := context.Background()
rc := NewContainer(&NewContainerInput{
Image: "alpine:latest",
Entrypoint: []string{"sleep", "30"},
Name: "act-test-symlink-" + time.Now().Format("20060102150405.000000"),
AutoRemove: true,
})
require.NoError(t, rc.Pull(false)(ctx))
require.NoError(t, rc.Create(nil, nil)(ctx))
require.NoError(t, rc.Start(false)(ctx))
t.Cleanup(func() {
_ = rc.Remove()(ctx)
_ = rc.Close()(ctx)
})
// CopyTarStream first creates the destination directory by extracting a tar at "/",
// which makes the daemon mkdir var, then var/run (the symlink), then act — the exact
// step that fails on the broken daemon.
err := rc.CopyTarStream(ctx, "/var/run/act/actions/", &bytes.Buffer{})
require.NoError(t, err)
}
// Type assert containerReference implements ExecutionsEnvironment // Type assert containerReference implements ExecutionsEnvironment
var _ ExecutionsEnvironment = &containerReference{} var _ ExecutionsEnvironment = &containerReference{}
-138
View File
@@ -1,138 +0,0 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// Copyright 2024 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"fmt"
"os"
"path/filepath"
"strings"
log "github.com/sirupsen/logrus"
)
var CommonSocketLocations = []string{
"/var/run/docker.sock",
"/run/podman/podman.sock",
"$HOME/.colima/docker.sock",
"$XDG_RUNTIME_DIR/docker.sock",
"$XDG_RUNTIME_DIR/podman/podman.sock",
`\\.\pipe\docker_engine`,
"$HOME/.docker/run/docker.sock",
}
// returns socket URI or false if not found any
func socketLocation() (string, bool) {
if dockerHost, exists := os.LookupEnv("DOCKER_HOST"); exists {
return dockerHost, true
}
for _, p := range CommonSocketLocations {
if _, err := os.Lstat(os.ExpandEnv(p)); err == nil {
if strings.HasPrefix(p, `\\.\`) {
return "npipe://" + filepath.ToSlash(os.ExpandEnv(p)), true
}
return "unix://" + filepath.ToSlash(os.ExpandEnv(p)), true
}
}
return "", false
}
// This function, `isDockerHostURI`, takes a string argument `daemonPath`. It checks if the
// `daemonPath` is a valid Docker host URI. It does this by checking if the scheme of the URI (the
// part before "://") contains only alphabetic characters. If it does, the function returns true,
// indicating that the `daemonPath` is a Docker host URI. If it doesn't, or if the "://" delimiter
// is not found in the `daemonPath`, the function returns false.
func isDockerHostURI(daemonPath string) bool {
if before, _, ok := strings.Cut(daemonPath, "://"); ok {
scheme := before
if strings.IndexFunc(scheme, func(r rune) bool {
return (r < 'a' || r > 'z') && (r < 'A' || r > 'Z')
}) == -1 {
return true
}
}
return false
}
type SocketAndHost struct {
Socket string
Host string
}
func GetSocketAndHost(containerSocket string) (SocketAndHost, error) {
log.Debugf("Handling container host and socket")
// Prefer DOCKER_HOST, don't override it
dockerHost, hasDockerHost := socketLocation()
socketHost := SocketAndHost{Socket: containerSocket, Host: dockerHost}
// ** socketHost.Socket cases **
// Case 1: User does _not_ want to mount a daemon socket (passes a dash)
// Case 2: User passes a filepath to the socket; is that even valid?
// Case 3: User passes a valid socket; do nothing
// Case 4: User omitted the flag; set a sane default
// ** DOCKER_HOST cases **
// Case A: DOCKER_HOST is set; use it, i.e. do nothing
// Case B: DOCKER_HOST is empty; use sane defaults
// Set host for sanity's sake, when the socket isn't useful
if !hasDockerHost && (socketHost.Socket == "-" || !isDockerHostURI(socketHost.Socket) || socketHost.Socket == "") {
// Cases: 1B, 2B, 4B
socket, found := socketLocation()
socketHost.Host = socket
hasDockerHost = found
}
// A - (dash) in socketHost.Socket means don't mount, preserve this value
// otherwise if socketHost.Socket is a filepath don't use it as socket
// Exit early if we're in an invalid state (e.g. when no DOCKER_HOST and user supplied "-", a dash or omitted)
if !hasDockerHost && socketHost.Socket != "" && !isDockerHostURI(socketHost.Socket) {
// Cases: 1B, 2B
// Should we early-exit here, since there is no host nor socket to talk to?
return SocketAndHost{}, fmt.Errorf("DOCKER_HOST was not set, couldn't be found in the usual locations, and the container daemon socket ('%s') is invalid", socketHost.Socket)
}
// Default to DOCKER_HOST if set
if socketHost.Socket == "" && hasDockerHost {
// Cases: 4A
log.Debugf("Defaulting container socket to DOCKER_HOST")
socketHost.Socket = socketHost.Host
}
// Set sane default socket location if user omitted it
if socketHost.Socket == "" {
// Cases: 4B
socket, _ := socketLocation()
// socket is empty if it isn't found, so assignment here is at worst a no-op
log.Debugf("Defaulting container socket to default '%s'", socket)
socketHost.Socket = socket
}
// Exit if both the DOCKER_HOST and socket are fulfilled
if hasDockerHost {
// Cases: 1A, 2A, 3A, 4A
if !isDockerHostURI(socketHost.Socket) {
// Cases: 1A, 2A
log.Debugf("DOCKER_HOST is set, but socket is invalid '%s'", socketHost.Socket)
}
return socketHost, nil
}
// Set a sane DOCKER_HOST default if we can
if isDockerHostURI(socketHost.Socket) {
// Cases: 3B
log.Debugf("Setting DOCKER_HOST to container socket '%s'", socketHost.Socket)
socketHost.Host = socketHost.Socket
// Both DOCKER_HOST and container socket are valid; short-circuit exit
return socketHost, nil
}
// Here there is no DOCKER_HOST _and_ the supplied container socket is not a valid URI (either invalid or a file path)
// Cases: 2B <- but is already handled at the top
// I.e. this path should never be taken
return SocketAndHost{}, fmt.Errorf("no DOCKER_HOST and an invalid container socket '%s'", socketHost.Socket)
}
-167
View File
@@ -1,167 +0,0 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// Copyright 2024 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"os"
"testing"
log "github.com/sirupsen/logrus"
assert "github.com/stretchr/testify/assert"
)
func init() {
log.SetLevel(log.DebugLevel)
}
var originalCommonSocketLocations = CommonSocketLocations
func isolateSocketEnv(t *testing.T) {
t.Helper()
t.Cleanup(func() { CommonSocketLocations = originalCommonSocketLocations })
if host, ok := os.LookupEnv("DOCKER_HOST"); ok {
t.Setenv("DOCKER_HOST", host)
} else {
t.Cleanup(func() { os.Unsetenv("DOCKER_HOST") })
}
}
func TestGetSocketAndHostWithSocket(t *testing.T) {
// Arrange
isolateSocketEnv(t)
dockerHost := "unix:///my/docker/host.sock"
socketURI := "/path/to/my.socket"
t.Setenv("DOCKER_HOST", dockerHost)
// Act
ret, err := GetSocketAndHost(socketURI)
// Assert
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{socketURI, dockerHost}, ret)
}
func TestGetSocketAndHostNoSocket(t *testing.T) {
// Arrange
dockerHost := "unix:///my/docker/host.sock"
t.Setenv("DOCKER_HOST", dockerHost)
// Act
ret, err := GetSocketAndHost("")
// Assert
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{dockerHost, dockerHost}, ret)
}
func TestGetSocketAndHostOnlySocket(t *testing.T) {
// Arrange
isolateSocketEnv(t)
socketURI := "/path/to/my.socket"
os.Unsetenv("DOCKER_HOST")
defaultSocket, defaultSocketFound := socketLocation()
// Act
ret, err := GetSocketAndHost(socketURI)
// Assert
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
assert.True(t, defaultSocketFound, "Expected to find default socket")
assert.Equal(t, socketURI, ret.Socket, "Expected socket to match common location")
assert.Equal(t, defaultSocket, ret.Host, "Expected ret.Host to match default socket location")
}
func TestGetSocketAndHostDontMount(t *testing.T) {
// Arrange
isolateSocketEnv(t)
dockerHost := "unix:///my/docker/host.sock"
t.Setenv("DOCKER_HOST", dockerHost)
// Act
ret, err := GetSocketAndHost("-")
// Assert
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{"-", dockerHost}, ret)
}
func TestGetSocketAndHostNoHostNoSocket(t *testing.T) {
// Arrange
isolateSocketEnv(t)
os.Unsetenv("DOCKER_HOST")
defaultSocket, found := socketLocation()
// Act
ret, err := GetSocketAndHost("")
// Assert
assert.True(t, found, "Expected a default socket to be found")
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{defaultSocket, defaultSocket}, ret, "Expected to match default socket location")
}
// Catch
// > Your code breaks setting DOCKER_HOST if shouldMount is false.
// > This happens if neither DOCKER_HOST nor --container-daemon-socket has a value, but socketLocation() returns a URI
func TestGetSocketAndHostNoHostNoSocketDefaultLocation(t *testing.T) {
// Arrange
isolateSocketEnv(t)
mySocketFile, tmpErr := os.CreateTemp(t.TempDir(), "act-*.sock")
mySocket := mySocketFile.Name()
unixSocket := "unix://" + mySocket
defer os.RemoveAll(mySocket)
assert.NoError(t, tmpErr) //nolint:testifylint // pre-existing issue from nektos/act
os.Unsetenv("DOCKER_HOST")
CommonSocketLocations = []string{mySocket}
defaultSocket, found := socketLocation()
// Act
ret, err := GetSocketAndHost("")
// Assert
assert.Equal(t, unixSocket, defaultSocket, "Expected default socket to match common socket location")
assert.True(t, found, "Expected default socket to be found")
assert.NoError(t, err, "Expected no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{unixSocket, unixSocket}, ret, "Expected to match default socket location")
}
func TestGetSocketAndHostNoHostInvalidSocket(t *testing.T) {
// Arrange
isolateSocketEnv(t)
os.Unsetenv("DOCKER_HOST")
mySocket := "/my/socket/path.sock"
CommonSocketLocations = []string{"/unusual", "/socket", "/location"}
defaultSocket, found := socketLocation()
// Act
ret, err := GetSocketAndHost(mySocket)
// Assert
assert.False(t, found, "Expected no default socket to be found")
assert.Equal(t, "", defaultSocket, "Expected no default socket to be found") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, SocketAndHost{}, ret, "Expected to match default socket location")
assert.Error(t, err, "Expected an error in invalid state")
}
func TestGetSocketAndHostOnlySocketValidButUnusualLocation(t *testing.T) {
// Arrange
isolateSocketEnv(t)
socketURI := "unix:///path/to/my.socket"
CommonSocketLocations = []string{"/unusual", "/location"}
os.Unsetenv("DOCKER_HOST")
defaultSocket, found := socketLocation()
// Act
ret, err := GetSocketAndHost(socketURI)
// Assert
// Default socket locations
assert.Equal(t, "", defaultSocket, "Expect default socket location to be empty") //nolint:testifylint // pre-existing issue from nektos/act
assert.False(t, found, "Expected no default socket to be found")
// Sane default
assert.NoError(t, err, "Expect no error from GetSocketAndHost") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, socketURI, ret.Host, "Expect host to default to unusual socket")
}
+3 -53
View File
@@ -26,6 +26,7 @@ import (
"gitea.com/gitea/runner/act/lookpath" "gitea.com/gitea/runner/act/lookpath"
"gitea.com/gitea/runner/internal/pkg/process" "gitea.com/gitea/runner/internal/pkg/process"
"github.com/creack/pty"
"github.com/go-git/go-billy/v5/helper/polyfill" "github.com/go-git/go-billy/v5/helper/polyfill"
"github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/go-git/go-git/v5/plumbing/format/gitignore"
@@ -71,12 +72,6 @@ func (e *HostEnvironment) Create(_, _ []string) common.Executor {
} }
} }
func (e *HostEnvironment) ConnectToNetwork(name string) common.Executor {
return func(ctx context.Context) error {
return nil
}
}
func (e *HostEnvironment) Close() common.Executor { func (e *HostEnvironment) Close() common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
return nil return nil
@@ -97,33 +92,6 @@ func (e *HostEnvironment) Copy(destPath string, files ...*FileEntry) common.Exec
} }
} }
func (e *HostEnvironment) CopyTarStream(ctx context.Context, destPath string, tarStream io.Reader) error {
if err := os.RemoveAll(destPath); err != nil {
return err
}
tr := tar.NewReader(tarStream)
cp := &filecollector.CopyCollector{
DstDir: destPath,
}
for {
ti, err := tr.Next()
if errors.Is(err, io.EOF) {
return nil
} else if err != nil {
return err
}
if ti.FileInfo().IsDir() {
continue
}
if ctx.Err() != nil {
return errors.New("CopyTarStream has been cancelled")
}
if err := cp.WriteFile(ti.Name, ti.FileInfo(), ti.Linkname, tr); err != nil {
return err
}
}
}
func (e *HostEnvironment) CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor { func (e *HostEnvironment) CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
@@ -142,7 +110,6 @@ func (e *HostEnvironment) CopyDir(destPath, srcPath string, useGitIgnore bool) c
ignorer = gitignore.NewMatcher(ps) ignorer = gitignore.NewMatcher(ps)
} }
fc := &filecollector.FileCollector{ fc := &filecollector.FileCollector{
Fs: &filecollector.DefaultFs{},
Ignorer: ignorer, Ignorer: ignorer,
SrcPath: srcPath, SrcPath: srcPath,
SrcPrefix: srcPrefix, SrcPrefix: srcPrefix,
@@ -180,7 +147,6 @@ func (e *HostEnvironment) GetContainerArchive(ctx context.Context, srcPath strin
srcPrefix += string(filepath.Separator) srcPrefix += string(filepath.Separator)
} }
fc := &filecollector.FileCollector{ fc := &filecollector.FileCollector{
Fs: &filecollector.DefaultFs{},
SrcPath: srcPath, SrcPath: srcPath,
SrcPrefix: srcPrefix, SrcPrefix: srcPrefix,
Handler: tc, Handler: tc,
@@ -246,24 +212,8 @@ func (w *ptyWriter) Write(buf []byte) (int, error) {
return w.Out.Write(buf) return w.Out.Write(buf)
} }
type localEnv struct {
env map[string]string
}
func (l *localEnv) Getenv(name string) string {
if runtime.GOOS == "windows" {
for k, v := range l.env {
if strings.EqualFold(name, k) {
return v
}
}
return ""
}
return l.env[name]
}
func lookupPathHost(cmd string, env map[string]string, writer io.Writer) (string, error) { func lookupPathHost(cmd string, env map[string]string, writer io.Writer) (string, error) {
f, err := lookpath.LookPath2(cmd, &localEnv{env: env}) f, err := lookpath.LookPath2(cmd, env)
if err != nil { if err != nil {
err := "Cannot find: " + cmd + " in PATH" err := "Cannot find: " + cmd + " in PATH"
if _, _err := writer.Write([]byte(err + "\n")); _err != nil { if _, _err := writer.Write([]byte(err + "\n")); _err != nil {
@@ -275,7 +225,7 @@ func lookupPathHost(cmd string, env map[string]string, writer io.Writer) (string
} }
func setupPty(cmd *exec.Cmd, cmdline string) (*os.File, *os.File, error) { func setupPty(cmd *exec.Cmd, cmdline string) (*os.File, *os.File, error) {
ppty, tty, err := openPty() ppty, tty, err := pty.Open()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
-17
View File
@@ -1,17 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build (!windows && !plan9 && !openbsd) || (!windows && !plan9 && !mips64)
package container
import (
"os"
"github.com/creack/pty"
)
func openPty() (*os.File, *os.File, error) {
return pty.Open()
}
-14
View File
@@ -1,14 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"errors"
"os"
)
func openPty() (*os.File, *os.File, error) {
return nil, nil, errors.New("Unsupported")
}
-14
View File
@@ -1,14 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"errors"
"os"
)
func openPty() (*os.File, *os.File, error) {
return nil, nil, errors.New("Unsupported")
}
-14
View File
@@ -1,14 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2022 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package container
import (
"errors"
"os"
)
func openPty() (*os.File, *os.File, error) {
return nil, nil, errors.New("Unsupported")
}
+10 -48
View File
@@ -97,55 +97,25 @@ type FileCollector struct {
Ignorer gitignore.Matcher Ignorer gitignore.Matcher
SrcPath string SrcPath string
SrcPrefix string SrcPrefix string
Fs Fs
Handler Handler Handler Handler
} }
type Fs interface { func openGitIndex(path string) (*index.Index, error) {
Walk(root string, fn filepath.WalkFunc) error repo, err := git.PlainOpen(path)
OpenGitIndex(path string) (*index.Index, error)
Open(path string) (io.ReadCloser, error)
Readlink(path string) (string, error)
}
type DefaultFs struct{}
func (*DefaultFs) Walk(root string, fn filepath.WalkFunc) error {
return filepath.Walk(root, fn)
}
func (*DefaultFs) OpenGitIndex(path string) (*index.Index, error) {
r, err := git.PlainOpen(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
i, err := r.Storer.Index() return repo.Storer.Index()
if err != nil {
return nil, err
}
return i, nil
}
func (*DefaultFs) Open(path string) (io.ReadCloser, error) {
return os.Open(path)
}
func (*DefaultFs) Readlink(path string) (string, error) {
return os.Readlink(path)
} }
func (fc *FileCollector) CollectFiles(ctx context.Context, submodulePath []string) filepath.WalkFunc { func (fc *FileCollector) CollectFiles(ctx context.Context, submodulePath []string) filepath.WalkFunc {
i, _ := fc.Fs.OpenGitIndex(path.Join(fc.SrcPath, path.Join(submodulePath...))) i, _ := openGitIndex(path.Join(fc.SrcPath, path.Join(submodulePath...)))
return func(file string, fi os.FileInfo, err error) error { return func(file string, fi os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
if ctx != nil { if ctx != nil && ctx.Err() != nil {
select {
case <-ctx.Done():
return errors.New("copy cancelled") return errors.New("copy cancelled")
default:
}
} }
sansPrefix := strings.TrimPrefix(file, fc.SrcPrefix) sansPrefix := strings.TrimPrefix(file, fc.SrcPrefix)
@@ -175,7 +145,7 @@ func (fc *FileCollector) CollectFiles(ctx context.Context, submodulePath []strin
} }
} }
if err == nil && entry.Mode == filemode.Submodule { if err == nil && entry.Mode == filemode.Submodule {
err = fc.Fs.Walk(file, fc.CollectFiles(ctx, split)) err = filepath.Walk(file, fc.CollectFiles(ctx, split))
if err != nil { if err != nil {
return err return err
} }
@@ -185,7 +155,7 @@ func (fc *FileCollector) CollectFiles(ctx context.Context, submodulePath []strin
// return on non-regular files (thanks to [kumo](https://medium.com/@komuw/just-like-you-did-fbdd7df829d3) for this suggested update) // return on non-regular files (thanks to [kumo](https://medium.com/@komuw/just-like-you-did-fbdd7df829d3) for this suggested update)
if fi.Mode()&os.ModeSymlink == os.ModeSymlink { if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
linkName, err := fc.Fs.Readlink(file) linkName, err := os.Readlink(file)
if err != nil { if err != nil {
return fmt.Errorf("unable to readlink '%s': %w", file, err) return fmt.Errorf("unable to readlink '%s': %w", file, err)
} }
@@ -195,23 +165,15 @@ func (fc *FileCollector) CollectFiles(ctx context.Context, submodulePath []strin
} }
// open file // open file
f, err := fc.Fs.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
return err return err
} }
defer f.Close() defer f.Close()
if ctx != nil { if ctx != nil {
// make io.Copy cancellable by closing the file stop := context.AfterFunc(ctx, func() { _ = f.Close() })
cpctx, cpfinish := context.WithCancel(ctx) defer stop()
defer cpfinish()
go func() {
select {
case <-cpctx.Done():
case <-ctx.Done():
f.Close()
}
}()
} }
return fc.Handler.WriteFile(path, fi, "", f) return fc.Handler.WriteFile(path, fi, "", f)
+46 -177
View File
@@ -6,6 +6,7 @@ package filecollector
import ( import (
"archive/tar" "archive/tar"
"bytes"
"context" "context"
"io" "io"
"os" "os"
@@ -13,110 +14,41 @@ import (
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
"time"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
git "github.com/go-git/go-git/v5" git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/go-git/go-git/v5/plumbing/format/gitignore"
"github.com/go-git/go-git/v5/plumbing/format/index"
"github.com/go-git/go-git/v5/storage/filesystem"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type memoryFs struct {
billy.Filesystem
}
func (mfs *memoryFs) walk(root string, fn filepath.WalkFunc) error {
dir, err := mfs.ReadDir(root)
if err != nil {
return err
}
for i := range dir {
filename := filepath.Join(root, dir[i].Name())
err = fn(filename, dir[i], nil)
if dir[i].IsDir() {
if err == filepath.SkipDir {
err = nil
} else if err := mfs.walk(filename, fn); err != nil {
return err
}
}
if err != nil {
return err
}
}
return nil
}
func (mfs *memoryFs) Walk(root string, fn filepath.WalkFunc) error {
stat, err := mfs.Lstat(root)
if err != nil {
return err
}
err = fn(strings.Join([]string{root, "."}, string(filepath.Separator)), stat, nil)
if err != nil {
return err
}
return mfs.walk(root, fn)
}
func (mfs *memoryFs) OpenGitIndex(path string) (*index.Index, error) {
f, _ := mfs.Filesystem.Chroot(filepath.Join(path, ".git")) //nolint:staticcheck // pre-existing issue from nektos/act
storage := filesystem.NewStorage(f, cache.NewObjectLRUDefault())
i, err := storage.Index()
if err != nil {
return nil, err
}
return i, nil
}
func (mfs *memoryFs) Open(path string) (io.ReadCloser, error) {
return mfs.Filesystem.Open(path)
}
func (mfs *memoryFs) Readlink(path string) (string, error) {
return mfs.Filesystem.Readlink(path)
}
func TestIgnoredTrackedfile(t *testing.T) { func TestIgnoredTrackedfile(t *testing.T) {
fs := memfs.New() repoDir := filepath.Join(t.TempDir(), "mygitrepo")
_ = fs.MkdirAll("mygitrepo/.git", 0o777) repo, err := git.PlainInit(repoDir, false)
dotgit, _ := fs.Chroot("mygitrepo/.git") require.NoError(t, err)
worktree, _ := fs.Chroot("mygitrepo") require.NoError(t, os.WriteFile(filepath.Join(repoDir, ".gitignore"), []byte(".*\n"), 0o644))
repo, _ := git.Init(filesystem.NewStorage(dotgit, cache.NewObjectLRUDefault()), worktree) require.NoError(t, os.WriteFile(filepath.Join(repoDir, ".env"), []byte("test=val1\n"), 0o644))
f, _ := worktree.Create(".gitignore") worktree, err := repo.Worktree()
_, _ = f.Write([]byte(".*\n")) require.NoError(t, err)
f.Close() _, err = worktree.Add(".gitignore")
// This file shouldn't be in the tar require.NoError(t, err)
f, _ = worktree.Create(".env")
_, _ = f.Write([]byte("test=val1\n"))
f.Close()
w, _ := repo.Worktree()
// .gitignore is in the tar after adding it to the index
_, _ = w.Add(".gitignore")
tmpTar, _ := fs.Create("temp.tar") var archive bytes.Buffer
tw := tar.NewWriter(tmpTar) tw := tar.NewWriter(&archive)
ps, _ := gitignore.ReadPatterns(worktree, []string{}) patterns, err := gitignore.ReadPatterns(worktree.Filesystem, nil)
ignorer := gitignore.NewMatcher(ps) require.NoError(t, err)
ignorer := gitignore.NewMatcher(patterns)
fc := &FileCollector{ fc := &FileCollector{
Fs: &memoryFs{Filesystem: fs},
Ignorer: ignorer, Ignorer: ignorer,
SrcPath: "mygitrepo", SrcPath: repoDir,
SrcPrefix: "mygitrepo" + string(filepath.Separator), SrcPrefix: repoDir + string(filepath.Separator),
Handler: &TarCollector{ Handler: &TarCollector{
TarWriter: tw, TarWriter: tw,
}, },
} }
err := fc.Fs.Walk("mygitrepo", fc.CollectFiles(context.Background(), []string{})) err = filepath.Walk(repoDir, fc.CollectFiles(context.Background(), nil))
assert.NoError(t, err, "successfully collect files") //nolint:testifylint // pre-existing issue from nektos/act assert.NoError(t, err, "successfully collect files")
tw.Close() require.NoError(t, tw.Close())
_, _ = tmpTar.Seek(0, io.SeekStart) tr := tar.NewReader(&archive)
tr := tar.NewReader(tmpTar)
h, err := tr.Next() h, err := tr.Next()
assert.NoError(t, err, "tar must not be empty") //nolint:testifylint // pre-existing issue from nektos/act assert.NoError(t, err, "tar must not be empty") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, ".gitignore", h.Name) assert.Equal(t, ".gitignore", h.Name)
@@ -125,47 +57,32 @@ func TestIgnoredTrackedfile(t *testing.T) {
} }
func TestSymlinks(t *testing.T) { func TestSymlinks(t *testing.T) {
fs := memfs.New() if runtime.GOOS == "windows" {
_ = fs.MkdirAll("mygitrepo/.git", 0o777) t.Skip("creating symlinks requires elevated privileges on Windows")
dotgit, _ := fs.Chroot("mygitrepo/.git") }
worktree, _ := fs.Chroot("mygitrepo") repoDir := filepath.Join(t.TempDir(), "mygitrepo")
repo, _ := git.Init(filesystem.NewStorage(dotgit, cache.NewObjectLRUDefault()), worktree) repo, err := git.PlainInit(repoDir, false)
// This file shouldn't be in the tar require.NoError(t, err)
f, err := worktree.Create(".env") require.NoError(t, os.WriteFile(filepath.Join(repoDir, ".env"), []byte("test=val1\n"), 0o644))
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act require.NoError(t, os.Symlink(".env", filepath.Join(repoDir, "test.env")))
_, err = f.Write([]byte("test=val1\n")) worktree, err := repo.Worktree()
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act require.NoError(t, err)
f.Close() _, err = worktree.Add("test.env")
err = worktree.Symlink(".env", "test.env") require.NoError(t, err)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
w, err := repo.Worktree() var archive bytes.Buffer
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act tw := tar.NewWriter(&archive)
// .gitignore is in the tar after adding it to the index
_, err = w.Add(".env")
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
_, err = w.Add("test.env")
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
tmpTar, _ := fs.Create("temp.tar")
tw := tar.NewWriter(tmpTar)
ps, _ := gitignore.ReadPatterns(worktree, []string{})
ignorer := gitignore.NewMatcher(ps)
fc := &FileCollector{ fc := &FileCollector{
Fs: &memoryFs{Filesystem: fs}, SrcPath: repoDir,
Ignorer: ignorer, SrcPrefix: repoDir + string(filepath.Separator),
SrcPath: "mygitrepo",
SrcPrefix: "mygitrepo" + string(filepath.Separator),
Handler: &TarCollector{ Handler: &TarCollector{
TarWriter: tw, TarWriter: tw,
}, },
} }
err = fc.Fs.Walk("mygitrepo", fc.CollectFiles(context.Background(), []string{})) err = filepath.Walk(repoDir, fc.CollectFiles(context.Background(), nil))
assert.NoError(t, err, "successfully collect files") //nolint:testifylint // pre-existing issue from nektos/act assert.NoError(t, err, "successfully collect files")
tw.Close() require.NoError(t, tw.Close())
_, _ = tmpTar.Seek(0, io.SeekStart) tr := tar.NewReader(&archive)
tr := tar.NewReader(tmpTar)
h, err := tr.Next() h, err := tr.Next()
files := map[string]tar.Header{} files := map[string]tar.Header{}
for err == nil { for err == nil {
@@ -223,62 +140,14 @@ func TestCopyCollectorWriteFileOverwritesFileWithSymlink(t *testing.T) {
assert.Equal(t, "target", resolved) assert.Equal(t, "target", resolved)
} }
func TestDefaultFsOpenReadlinkAndWalk(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("creating symlinks requires elevated privileges on Windows")
}
root := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(root, "file.txt"), []byte("content"), 0o644))
require.NoError(t, os.Symlink("file.txt", filepath.Join(root, "link.txt")))
fsys := &DefaultFs{}
var walked []string
require.NoError(t, fsys.Walk(root, func(path string, info os.FileInfo, err error) error {
require.NoError(t, err)
walked = append(walked, info.Name())
return nil
}))
require.Contains(t, walked, "file.txt")
require.Contains(t, walked, "link.txt")
file, err := fsys.Open(filepath.Join(root, "file.txt"))
require.NoError(t, err)
data, err := io.ReadAll(file)
require.NoError(t, err)
require.NoError(t, file.Close())
require.Equal(t, "content", string(data))
link, err := fsys.Readlink(filepath.Join(root, "link.txt"))
require.NoError(t, err)
require.Equal(t, "file.txt", link)
}
func TestFileCollectorCancellationAndWalkError(t *testing.T) { func TestFileCollectorCancellationAndWalkError(t *testing.T) {
fc := &FileCollector{Fs: &memoryFs{Filesystem: memfs.New()}} ctx, cancel := context.WithCancel(t.Context())
walk := fc.CollectFiles(cancelledContext(t), nil) cancel()
walk := (&FileCollector{}).CollectFiles(ctx, nil)
err := walk("file", fakeFileInfo{name: "file"}, nil) err := walk("file", nil, nil)
require.EqualError(t, err, "copy cancelled") require.EqualError(t, err, "copy cancelled")
err = walk("file", fakeFileInfo{name: "file"}, os.ErrPermission) err = walk("file", nil, os.ErrPermission)
require.ErrorIs(t, err, os.ErrPermission) require.ErrorIs(t, err, os.ErrPermission)
} }
func cancelledContext(t *testing.T) context.Context {
t.Helper()
ctx, cancel := context.WithCancel(context.Background())
cancel()
return ctx
}
type fakeFileInfo struct {
name string
}
func (f fakeFileInfo) Name() string { return f.name }
func (f fakeFileInfo) Size() int64 { return 0 }
func (f fakeFileInfo) Mode() os.FileMode { return 0o644 }
func (f fakeFileInfo) ModTime() time.Time { return time.Time{} }
func (f fakeFileInfo) IsDir() bool { return false }
func (f fakeFileInfo) Sys() any { return nil }
+13 -32
View File
@@ -25,32 +25,9 @@ var (
findGithubRepo = git.FindGithubRepo findGithubRepo = git.FindGithubRepo
) )
func withDefaultBranch(ctx context.Context, b string, event map[string]any) map[string]any {
repoI, ok := event["repository"]
if !ok {
repoI = make(map[string]any)
}
repo, ok := repoI.(map[string]any)
if !ok {
common.Logger(ctx).Warnf("unable to set default branch to %v", b)
return event
}
// if the branch is already there return with no changes
if _, ok = repo["default_branch"]; ok {
return event
}
repo["default_branch"] = b
event["repository"] = repo
return event
}
// SetRef resolves the ref of the context from its event payload, falling back // SetRef resolves the ref of the context from its event payload, falling back
// to the ref checked out in repoPath. // to the ref checked out in repoPath.
func SetRef(ctx context.Context, ghc *model.GithubContext, defaultBranch, repoPath string) { func SetRef(ctx context.Context, ghc *model.GithubContext, repoPath string) {
logger := common.Logger(ctx) logger := common.Logger(ctx)
// https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows // https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows
@@ -82,11 +59,15 @@ func SetRef(ctx context.Context, ghc *model.GithubContext, defaultBranch, repoPa
ghc.Ref = ref ghc.Ref = ref
} }
// set the branch in the event data repository, exists := ghc.Event["repository"]
if defaultBranch != "" { if !exists {
ghc.Event = withDefaultBranch(ctx, defaultBranch, ghc.Event) repository = map[string]any{}
} else { }
ghc.Event = withDefaultBranch(ctx, "master", ghc.Event) if repository, ok := repository.(map[string]any); !ok {
logger.Warn("unable to set default branch to master")
} else if _, exists := repository["default_branch"]; !exists {
repository["default_branch"] = "master"
ghc.Event["repository"] = repository
} }
if ghc.Ref == "" { if ghc.Ref == "" {
@@ -125,11 +106,11 @@ func SetSha(ctx context.Context, ghc *model.GithubContext, repoPath string) {
// SetRepositoryAndOwner resolves the repository of the context from the git // SetRepositoryAndOwner resolves the repository of the context from the git
// remote in repoPath when it is not set yet, and derives its owner. // remote in repoPath when it is not set yet, and derives its owner.
func SetRepositoryAndOwner(ctx context.Context, ghc *model.GithubContext, githubInstance, remoteName, repoPath string) { func SetRepositoryAndOwner(ctx context.Context, ghc *model.GithubContext, githubInstance, repoPath string) {
if ghc.Repository == "" { if ghc.Repository == "" {
repo, err := findGithubRepo(ctx, repoPath, githubInstance, remoteName) repo, err := findGithubRepo(ctx, repoPath, githubInstance)
if err != nil { if err != nil {
common.Logger(ctx).Warningf("unable to get git repo (githubInstance: %v; remoteName: %v, repoPath: %v): %v", githubInstance, remoteName, repoPath, err) common.Logger(ctx).Warningf("unable to get git repo (githubInstance: %v, repoPath: %v): %v", githubInstance, repoPath, err)
return return
} }
ghc.Repository = repo ghc.Repository = repo
+2 -2
View File
@@ -104,7 +104,7 @@ func TestSetRef(t *testing.T) {
Event: table.event, Event: table.event,
} }
SetRef(context.Background(), ghc, "main", "/some/dir") SetRef(context.Background(), ghc, "/some/dir")
ghc.SetRefTypeAndName() ghc.SetRefTypeAndName()
assert.Equal(t, table.ref, ghc.Ref) assert.Equal(t, table.ref, ghc.Ref)
@@ -122,7 +122,7 @@ func TestSetRef(t *testing.T) {
Event: map[string]any{}, Event: map[string]any{},
} }
SetRef(context.Background(), ghc, "", "/some/dir") SetRef(context.Background(), ghc, "/some/dir")
assert.Equal(t, "refs/heads/master", ghc.Ref) assert.Equal(t, "refs/heads/master", ghc.Ref)
}) })
+14 -2
View File
@@ -4,6 +4,18 @@
package lookpath package lookpath
type Env interface { import (
Getenv(name string) string "runtime"
"strings"
)
func getenv(env map[string]string, name string) string {
if runtime.GOOS == "windows" {
for key, value := range env {
if strings.EqualFold(name, key) {
return value
}
}
}
return env[name]
} }
+1 -1
View File
@@ -18,7 +18,7 @@ var ErrNotFound = errors.New("executable file not found in $PATH")
// directories named by the PATH environment variable. // directories named by the PATH environment variable.
// If file contains a slash, it is tried directly and the PATH is not consulted. // If file contains a slash, it is tried directly and the PATH is not consulted.
// The result may be an absolute path or a path relative to the current directory. // The result may be an absolute path or a path relative to the current directory.
func LookPath2(file string, lenv Env) (string, error) { func LookPath2(file string, _ map[string]string) (string, error) {
// Wasm can not execute processes, so act as if there are no executables at all. // Wasm can not execute processes, so act as if there are no executables at all.
return "", &Error{file, ErrNotFound} return "", &Error{file, ErrNotFound}
} }
+2 -2
View File
@@ -32,7 +32,7 @@ func findExecutable(file string) error {
// If file begins with "/", "#", "./", or "../", it is tried // If file begins with "/", "#", "./", or "../", it is tried
// directly and the path is not consulted. // directly and the path is not consulted.
// The result may be an absolute path or a path relative to the current directory. // The result may be an absolute path or a path relative to the current directory.
func LookPath2(file string, lenv Env) (string, error) { func LookPath2(file string, env map[string]string) (string, error) {
// skip the path lookup for these prefixes // skip the path lookup for these prefixes
skip := []string{"/", "#", "./", "../"} skip := []string{"/", "#", "./", "../"}
@@ -46,7 +46,7 @@ func LookPath2(file string, lenv Env) (string, error) {
} }
} }
path := lenv.Getenv("path") path := getenv(env, "path")
for _, dir := range filepath.SplitList(path) { for _, dir := range filepath.SplitList(path) {
path := filepath.Join(dir, file) path := filepath.Join(dir, file)
if err := findExecutable(path); err == nil { if err := findExecutable(path); err == nil {
+2 -2
View File
@@ -33,7 +33,7 @@ func findExecutable(file string) error {
// directories named by the PATH environment variable. // directories named by the PATH environment variable.
// If file contains a slash, it is tried directly and the PATH is not consulted. // If file contains a slash, it is tried directly and the PATH is not consulted.
// The result may be an absolute path or a path relative to the current directory. // The result may be an absolute path or a path relative to the current directory.
func LookPath2(file string, lenv Env) (string, error) { func LookPath2(file string, env map[string]string) (string, error) {
// NOTE(rsc): I wish we could use the Plan 9 behavior here // NOTE(rsc): I wish we could use the Plan 9 behavior here
// (only bypass the path if file begins with / or ./ or ../) // (only bypass the path if file begins with / or ./ or ../)
// but that would not match all the Unix shells. // but that would not match all the Unix shells.
@@ -45,7 +45,7 @@ func LookPath2(file string, lenv Env) (string, error) {
} }
return "", &Error{file, err} return "", &Error{file, err}
} }
path := lenv.Getenv("PATH") path := getenv(env, "PATH")
for _, dir := range filepath.SplitList(path) { for _, dir := range filepath.SplitList(path) {
if dir == "" { if dir == "" {
// Unix shell semantics: path element "" means "." // Unix shell semantics: path element "" means "."
+4 -10
View File
@@ -13,12 +13,6 @@ import (
"testing" "testing"
) )
type testEnv map[string]string
func (e testEnv) Getenv(name string) string {
return e[name]
}
func TestLookPath2SearchesPathAndEmptyElement(t *testing.T) { func TestLookPath2SearchesPathAndEmptyElement(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
exe := filepath.Join(dir, "tool") exe := filepath.Join(dir, "tool")
@@ -26,7 +20,7 @@ func TestLookPath2SearchesPathAndEmptyElement(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
got, err := LookPath2("tool", testEnv{"PATH": string(filepath.ListSeparator) + dir}) got, err := LookPath2("tool", map[string]string{"PATH": string(filepath.ListSeparator) + dir})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -42,7 +36,7 @@ func TestLookPath2DirectPathDoesNotSearchPath(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
got, err := LookPath2(exe, testEnv{"PATH": ""}) got, err := LookPath2(exe, map[string]string{"PATH": ""})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -58,7 +52,7 @@ func TestLookPath2ReportsPermissionAndNotFound(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
_, err := LookPath2(file, testEnv{"PATH": dir}) _, err := LookPath2(file, map[string]string{"PATH": dir})
var pathErr *Error var pathErr *Error
if !errors.As(err, &pathErr) || !errors.Is(pathErr.Err, fs.ErrPermission) { if !errors.As(err, &pathErr) || !errors.Is(pathErr.Err, fs.ErrPermission) {
t.Fatalf("LookPath2(non-executable) error = %v, want fs.ErrPermission wrapped in *Error", err) t.Fatalf("LookPath2(non-executable) error = %v, want fs.ErrPermission wrapped in *Error", err)
@@ -67,7 +61,7 @@ func TestLookPath2ReportsPermissionAndNotFound(t *testing.T) {
t.Fatalf("Error() = %q, want %q", pathErr.Error(), fs.ErrPermission.Error()) t.Fatalf("Error() = %q, want %q", pathErr.Error(), fs.ErrPermission.Error())
} }
_, err = LookPath2("missing", testEnv{"PATH": dir}) _, err = LookPath2("missing", map[string]string{"PATH": dir})
if !errors.As(err, &pathErr) || !errors.Is(pathErr.Err, ErrNotFound) { if !errors.As(err, &pathErr) || !errors.Is(pathErr.Err, ErrNotFound) {
t.Fatalf("LookPath2(missing) error = %v, want ErrNotFound wrapped in *Error", err) t.Fatalf("LookPath2(missing) error = %v, want ErrNotFound wrapped in *Error", err)
} }
+3 -3
View File
@@ -58,9 +58,9 @@ func findExecutable(file string, exts []string) (string, error) {
// LookPath also uses PATHEXT environment variable to match // LookPath also uses PATHEXT environment variable to match
// a suitable candidate. // a suitable candidate.
// The result may be an absolute path or a path relative to the current directory. // The result may be an absolute path or a path relative to the current directory.
func LookPath2(file string, lenv Env) (string, error) { func LookPath2(file string, env map[string]string) (string, error) {
var exts []string var exts []string
x := lenv.Getenv(`PATHEXT`) x := getenv(env, `PATHEXT`)
if x != "" { if x != "" {
for e := range strings.SplitSeq(strings.ToLower(x), `;`) { for e := range strings.SplitSeq(strings.ToLower(x), `;`) {
if e == "" { if e == "" {
@@ -85,7 +85,7 @@ func LookPath2(file string, lenv Env) (string, error) {
if f, err := findExecutable(filepath.Join(".", file), exts); err == nil { if f, err := findExecutable(filepath.Join(".", file), exts); err == nil {
return f, nil return f, nil
} }
path := lenv.Getenv("path") path := getenv(env, "path")
for _, dir := range filepath.SplitList(path) { for _, dir := range filepath.SplitList(path) {
if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil { if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil {
return f, nil return f, nil
+22 -79
View File
@@ -124,21 +124,9 @@ func readActionImpl(ctx context.Context, step *model.Step, actionDir, actionPath
defer closer.Close() defer closer.Close()
action, err := model.ReadAction(reader) action, err := model.ReadAction(reader)
// For Gitea, reduce log noise
// logger.Debugf("Read action %v from '%s'", action, "Unknown")
return action, err return action, err
} }
// cachedActionTar returns the action's tree from the action cache, which only a remote action
// has an entry in.
func cachedActionTar(ctx context.Context, step actionStep, name, includePrefix string) (io.ReadCloser, error) {
remote, ok := step.(*stepActionRemote)
if !ok {
return nil, fmt.Errorf("action %q is a remote action but runs as %T", name, step)
}
return step.getRunContext().Config.ActionCache.GetTarArchive(ctx, remote.cacheDir, remote.resolvedSha, includePrefix)
}
func maybeCopyToActionDir(ctx context.Context, step actionStep, actionDir, actionPath, containerActionDir string) error { func maybeCopyToActionDir(ctx context.Context, step actionStep, actionDir, actionPath, containerActionDir string) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
rc := step.getRunContext() rc := step.getRunContext()
@@ -148,23 +136,13 @@ func maybeCopyToActionDir(ctx context.Context, step actionStep, actionDir, actio
return nil return nil
} }
var containerActionDirCopy string containerActionDirCopy := strings.TrimSuffix(containerActionDir, actionPath)
containerActionDirCopy = strings.TrimSuffix(containerActionDir, actionPath)
logger.Debug(containerActionDirCopy) logger.Debug(containerActionDirCopy)
if !strings.HasSuffix(containerActionDirCopy, `/`) { if !strings.HasSuffix(containerActionDirCopy, `/`) {
containerActionDirCopy += `/` containerActionDirCopy += `/`
} }
if rc.Config != nil && rc.Config.ActionCache != nil {
ta, err := cachedActionTar(ctx, step, stepModel.Uses, "")
if err != nil {
return err
}
defer ta.Close()
return rc.JobContainer.CopyTarStream(ctx, containerActionDirCopy, ta)
}
defer git.AcquireCloneLock(actionDir)() defer git.AcquireCloneLock(actionDir)()
if !rc.Config.NoActionPatch { if !rc.Config.NoActionPatch {
@@ -191,13 +169,10 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
} }
action := step.getActionModel() action := step.getActionModel()
// For Gitea, reduce log noise
// logger.Debugf("About to run action %v", action)
err := setupActionEnv(ctx, step, remoteAction) rc.withGithubEnv(ctx, step.getGithubContext(ctx), *step.getEnv())
if err != nil { populateEnvsFromSavedState(step.getEnv(), step, rc)
return err populateEnvsFromInput(ctx, step.getEnv(), action, rc)
}
actionLocation := path.Join(actionDir, actionPath) actionLocation := path.Join(actionDir, actionPath)
actionName, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc) actionName, containerActionDir := getContainerActionPaths(stepModel, actionLocation, rc)
@@ -215,7 +190,7 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
rc.ApplyExtraPath(ctx, step.getEnv()) rc.ApplyExtraPath(ctx, step.getEnv())
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx) return rc.JobContainer.Exec(containerArgs, *step.getEnv(), "", "")(ctx)
case x.IsDocker(): case x.IsDocker():
location := actionLocation location := actionLocation
if remoteAction == nil { if remoteAction == nil {
@@ -240,8 +215,8 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
execArgs := []string{filepath.Join(containerActionDir, execFileName)} execArgs := []string{filepath.Join(containerActionDir, execFileName)}
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
rc.execJobContainer(buildArgs, *step.getEnv(), "", containerActionDir), rc.JobContainer.Exec(buildArgs, *step.getEnv(), "", containerActionDir),
rc.execJobContainer(execArgs, *step.getEnv(), "", ""), rc.JobContainer.Exec(execArgs, *step.getEnv(), "", ""),
)(ctx) )(ctx)
default: default:
return fmt.Errorf("The runs.using key must be one of: %v, got %s", []string{ return fmt.Errorf("The runs.using key must be one of: %v, got %s", []string{
@@ -257,20 +232,6 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
} }
} }
func setupActionEnv(ctx context.Context, step actionStep, _ *remoteAction) error {
rc := step.getRunContext()
// A few fields in the environment (e.g. GITHUB_ACTION_REPOSITORY)
// are dependent on the action. That means we can complete the
// setup only after resolving the whole action model and cloning
// the action
rc.withGithubEnv(ctx, step.getGithubContext(ctx), *step.getEnv())
populateEnvsFromSavedState(step.getEnv(), step, rc)
populateEnvsFromInput(ctx, step.getEnv(), step.getActionModel(), rc)
return nil
}
// https://github.com/nektos/act/issues/228#issuecomment-629709055 // https://github.com/nektos/act/issues/228#issuecomment-629709055
// files in .gitignore are not copied in a Docker container // files in .gitignore are not copied in a Docker container
// this causes issues with actions that ignore other important resources // this causes issues with actions that ignore other important resources
@@ -364,12 +325,6 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, b
return err return err
} }
defer buildContext.Close() defer buildContext.Close()
} else if rc.Config.ActionCache != nil {
buildContext, err = cachedActionTar(ctx, step, actionName, contextDir)
if err != nil {
return err
}
defer buildContext.Close()
} }
prepImage = ContainerNewDockerBuildExecutor(container.NewDockerBuildExecutorInput{ prepImage = ContainerNewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
ContextDir: contextDir, ContextDir: contextDir,
@@ -404,21 +359,19 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, b
if err != nil { if err != nil {
return err return err
} }
stepContainer := newStepContainer(ctx, step, image, cmd, entrypoint) stepContainer := newStepContainer(ctx, step, image, cmd, entrypoint, rc.Config.ContainerOptions)
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
prepImage, prepImage,
stepContainer.Pull(forcePull), stepContainer.Pull(forcePull),
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers), stepContainer.Remove(),
stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop), stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
stepContainer.Start(true), stepContainer.Start(true),
).Finally(
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers && !rc.Config.AutoRemove),
).Finally(stepContainer.Close())(ctx) ).Finally(stepContainer.Close())(ctx)
} }
// dockerEntrypoint returns the entrypoint the action's image runs with for the given // dockerEntrypoint returns the entrypoint the action's image runs with for the given
// stage. Only the main stage honours the `entrypoint` input. // stage. Only the main stage honours the `entrypoint` input.
func dockerEntrypoint(ctx context.Context, step actionStep, eval ExpressionEvaluator, stage stepStage) ([]string, error) { func dockerEntrypoint(ctx context.Context, step actionStep, eval *expressionEvaluator, stage stepStage) ([]string, error) {
runs := step.getActionModel().Runs runs := step.getActionModel().Runs
var entrypoint string var entrypoint string
@@ -469,18 +422,9 @@ func evalDockerArgs(ctx context.Context, step step, action *model.Action, cmd *[
} }
} }
func newStepContainer(ctx context.Context, step step, image string, cmd, entrypoint []string) container.Container { func newStepContainer(ctx context.Context, step step, image string, cmd, entrypoint []string, options string) container.Container {
rc := step.getRunContext() rc := step.getRunContext()
stepModel := step.getStepModel() logWriter := rc.commandLogWriter(ctx)
rawLogger := common.Logger(ctx).WithField("raw_output", true)
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
if rc.Config.LogOutput {
rawLogger.Infof("%s", s)
} else {
rawLogger.Debugf("%s", s)
}
return true
})
envList := make([]string, 0) envList := make([]string, 0)
for k, v := range *step.getEnv() { for k, v := range *step.getEnv() {
envList = append(envList, fmt.Sprintf("%s=%s", k, v)) envList = append(envList, fmt.Sprintf("%s=%s", k, v))
@@ -493,12 +437,12 @@ func newStepContainer(ctx context.Context, step step, image string, cmd, entrypo
if rc.IsHostEnv(ctx) { if rc.IsHostEnv(ctx) {
networkMode = "default" networkMode = "default"
} }
stepContainer := ContainerNewContainer(&container.NewContainerInput{ return ContainerNewContainer(&container.NewContainerInput{
Cmd: cmd, Cmd: cmd,
Entrypoint: entrypoint, Entrypoint: entrypoint,
WorkingDir: rc.JobContainer.ToContainerPath(rc.Config.Workdir), WorkingDir: rc.JobContainer.ToContainerPath(rc.Config.Workdir),
Image: image, Image: image,
Name: createContainerName(rc.jobContainerName(), "STEP-"+stepModel.ID), Name: createContainerName(rc.jobContainerName(), "STEP-"+step.getStepModel().ID),
Env: envList, Env: envList,
Mounts: mounts, Mounts: mounts,
NetworkMode: networkMode, NetworkMode: networkMode,
@@ -508,12 +452,11 @@ func newStepContainer(ctx context.Context, step step, image string, cmd, entrypo
Privileged: rc.Config.Privileged, Privileged: rc.Config.Privileged,
UsernsMode: rc.Config.UsernsMode, UsernsMode: rc.Config.UsernsMode,
Platform: rc.Config.ContainerArchitecture, Platform: rc.Config.ContainerArchitecture,
Options: rc.Config.ContainerOptions, Options: options,
AutoRemove: rc.Config.AutoRemove, AutoRemove: true,
ValidVolumes: rc.validVolumes(), ValidVolumes: rc.validVolumes(),
AllocatePTY: rc.Config.AllocatePTY, AllocatePTY: rc.Config.AllocatePTY,
}) })
return stepContainer
} }
func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) { func populateEnvsFromSavedState(env *map[string]string, step actionStep, rc *RunContext) {
@@ -648,7 +591,7 @@ func runPreStep(step actionStep) common.Executor {
rc.ApplyExtraPath(ctx, step.getEnv()) rc.ApplyExtraPath(ctx, step.getEnv())
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx) return rc.JobContainer.Exec(containerArgs, *step.getEnv(), "", "")(ctx)
case x.IsDocker(): case x.IsDocker():
// defaults in pre steps were missing, however provided inputs are available // defaults in pre steps were missing, however provided inputs are available
@@ -681,8 +624,8 @@ func runPreStep(step actionStep) common.Executor {
execArgs := []string{filepath.Join(containerActionDir, execFileName)} execArgs := []string{filepath.Join(containerActionDir, execFileName)}
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
rc.execJobContainer(buildArgs, *step.getEnv(), "", containerActionDir), rc.JobContainer.Exec(buildArgs, *step.getEnv(), "", containerActionDir),
rc.execJobContainer(execArgs, *step.getEnv(), "", ""), rc.JobContainer.Exec(execArgs, *step.getEnv(), "", ""),
)(ctx) )(ctx)
default: default:
return nil return nil
@@ -749,7 +692,7 @@ func runPostStep(step actionStep) common.Executor {
rc.ApplyExtraPath(ctx, step.getEnv()) rc.ApplyExtraPath(ctx, step.getEnv())
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx) return rc.JobContainer.Exec(containerArgs, *step.getEnv(), "", "")(ctx)
case x.IsDocker(): case x.IsDocker():
populateEnvsFromSavedState(step.getEnv(), step, rc) populateEnvsFromSavedState(step.getEnv(), step, rc)
@@ -775,8 +718,8 @@ func runPostStep(step actionStep) common.Executor {
execArgs := []string{filepath.Join(containerActionDir, execFileName)} execArgs := []string{filepath.Join(containerActionDir, execFileName)}
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
rc.execJobContainer(buildArgs, *step.getEnv(), "", containerActionDir), rc.JobContainer.Exec(buildArgs, *step.getEnv(), "", containerActionDir),
rc.execJobContainer(execArgs, *step.getEnv(), "", ""), rc.JobContainer.Exec(execArgs, *step.getEnv(), "", ""),
)(ctx) )(ctx)
default: default:
-156
View File
@@ -1,156 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2023 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package runner
import (
"archive/tar"
"context"
"crypto/rand"
"encoding/hex"
"errors"
"io"
"io/fs"
"path"
"strings"
git "github.com/go-git/go-git/v5"
config "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
)
type ActionCache interface {
Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error)
GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error)
}
type GoGitActionCache struct {
Path string
}
func (c GoGitActionCache) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) {
gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git")
gogitrepo, err := git.PlainInit(gitPath, true)
if errors.Is(err, git.ErrRepositoryAlreadyExists) {
gogitrepo, err = git.PlainOpen(gitPath)
}
if err != nil {
return "", err
}
tmpBranch := make([]byte, 12)
if _, err := rand.Read(tmpBranch); err != nil {
return "", err
}
branchName := hex.EncodeToString(tmpBranch)
var auth transport.AuthMethod
if token != "" {
auth = &http.BasicAuth{
Username: "token",
Password: token,
}
}
remote, err := gogitrepo.CreateRemoteAnonymous(&config.RemoteConfig{
Name: "anonymous",
URLs: []string{
url,
},
})
if err != nil {
return "", err
}
defer func() {
_ = gogitrepo.DeleteBranch(branchName)
}()
if err := remote.FetchContext(ctx, &git.FetchOptions{
RefSpecs: []config.RefSpec{
config.RefSpec(ref + ":" + branchName),
},
Auth: auth,
Force: true,
}); err != nil {
return "", err
}
hash, err := gogitrepo.ResolveRevision(plumbing.Revision(branchName))
if err != nil {
return "", err
}
return hash.String(), nil
}
func (c GoGitActionCache) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) {
gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git")
gogitrepo, err := git.PlainOpen(gitPath)
if err != nil {
return nil, err
}
commit, err := gogitrepo.CommitObject(plumbing.NewHash(sha))
if err != nil {
return nil, err
}
files, err := commit.Files()
if err != nil {
return nil, err
}
rpipe, wpipe := io.Pipe()
// Interrupt io.Copy using ctx
ch := make(chan int, 1)
go func() {
select {
case <-ctx.Done():
wpipe.CloseWithError(ctx.Err())
case <-ch:
}
}()
go func() {
defer wpipe.Close()
defer close(ch)
tw := tar.NewWriter(wpipe)
cleanIncludePrefix := path.Clean(includePrefix)
wpipe.CloseWithError(files.ForEach(func(f *object.File) error {
if err := ctx.Err(); err != nil {
return err
}
name := f.Name
if strings.HasPrefix(name, cleanIncludePrefix+"/") {
name = name[len(cleanIncludePrefix)+1:]
} else if cleanIncludePrefix != "." && name != cleanIncludePrefix {
return nil
}
fmode, err := f.Mode.ToOSFileMode()
if err != nil {
return err
}
if fmode&fs.ModeSymlink == fs.ModeSymlink {
content, err := f.Contents()
if err != nil {
return err
}
return tw.WriteHeader(&tar.Header{
Name: name,
Mode: int64(fmode),
Linkname: content,
})
}
err = tw.WriteHeader(&tar.Header{
Name: name,
Mode: int64(fmode),
Size: f.Size,
})
if err != nil {
return err
}
reader, err := f.Reader()
if err != nil {
return err
}
_, err = io.Copy(tw, reader)
return err
}))
}()
return rpipe, err
}
-157
View File
@@ -1,157 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2023 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package runner
import (
"archive/tar"
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"gitea.com/gitea/runner/act/common"
"gitea.dev/actionslib/pkg/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func runGit(t *testing.T, dir string, args ...string) {
t.Helper()
if dir != "" {
args = append([]string{"-C", dir}, args...)
}
cmd := exec.Command("git", args...)
// Fixed identity and host-config isolation so commits succeed offline regardless of the
// host's git config (mirrors gitCmd in act/common/git).
cmd.Env = append(os.Environ(),
"GIT_AUTHOR_NAME=test", "GIT_AUTHOR_EMAIL=test@example.com",
"GIT_COMMITTER_NAME=test", "GIT_COMMITTER_EMAIL=test@example.com",
"GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null",
)
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
}
// TestShortShaActionRejected verifies a `uses` ref that is a shortened commit SHA is rejected
// with a clear error. The action is resolved from a local repo (via DefaultActionInstance) so
// this runs offline.
func TestShortShaActionRejected(t *testing.T) {
// a local "remote" action repo at <root>/actions/hello-world-docker-action
actionRoot := t.TempDir()
repo := filepath.Join(actionRoot, "actions", "hello-world-docker-action")
require.NoError(t, os.MkdirAll(repo, 0o755))
runGit(t, "", "init", "--initial-branch=main", repo)
require.NoError(t, os.WriteFile(filepath.Join(repo, "action.yml"),
[]byte("name: hello\nruns:\n using: node24\n main: index.js\n"), 0o644))
runGit(t, repo, "add", ".")
runGit(t, repo, "commit", "-m", "initial")
out, err := exec.Command("git", "-C", repo, "rev-parse", "HEAD").Output()
require.NoError(t, err)
shortSha := strings.TrimSpace(string(out))[:7]
// a workflow that uses the action at the short SHA
wfDir := filepath.Join(t.TempDir(), "wf")
require.NoError(t, os.MkdirAll(wfDir, 0o755))
wf := fmt.Sprintf("on: push\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/hello-world-docker-action@%s\n", shortSha)
require.NoError(t, os.WriteFile(filepath.Join(wfDir, "push.yml"), []byte(wf), 0o644))
runner, err := New(&Config{
Workdir: wfDir,
EventName: "push",
Platforms: map[string]string{"ubuntu-latest": baseImage},
GitHubInstance: "github.com",
DefaultActionInstance: actionRoot,
ContainerMaxLifetime: time.Hour,
})
require.NoError(t, err)
planner, err := model.NewWorkflowPlanner(wfDir, true)
require.NoError(t, err)
plan, err := planner.PlanEvent("push")
require.NoError(t, err)
err = runner.NewPlanExecutor(plan)(common.WithDryrun(context.Background(), true))
require.Error(t, err)
assert.Contains(t, err.Error(), "shortened version of a commit SHA")
}
func TestActionCache(t *testing.T) {
a := assert.New(t)
ctx := context.Background()
// Build a local bare repo with a `js` action dir so this runs offline (formerly cloned
// github.com/nektos/act-test-actions over the network). allowAnySHA1InWant lets the
// "Fetch Sha" case fetch a commit hash directly.
remoteDir := t.TempDir()
runGit(t, "", "init", "--bare", "--initial-branch=main", remoteDir)
runGit(t, remoteDir, "config", "uploadpack.allowAnySHA1InWant", "true")
workDir := t.TempDir()
runGit(t, "", "clone", remoteDir, workDir)
require.NoError(t, os.MkdirAll(filepath.Join(workDir, "js"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(workDir, "js", "action.yml"),
[]byte("name: js\nruns:\n using: node24\n main: index.js\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(workDir, "js", "index.js"),
[]byte("console.log('hello');\n"), 0o644))
runGit(t, workDir, "add", ".")
runGit(t, workDir, "commit", "-m", "initial")
runGit(t, workDir, "push", "-u", "origin", "main")
out, err := exec.Command("git", "-C", workDir, "rev-parse", "main").Output()
require.NoError(t, err)
fullSha := strings.TrimSpace(string(out))
cache := &GoGitActionCache{
Path: t.TempDir(),
}
cacheDir := "local/act-test-actions"
refs := []struct {
Name string
Ref string
}{
{Name: "Fetch Branch Name", Ref: "main"},
{Name: "Fetch Branch Name Absolutely", Ref: "refs/heads/main"},
{Name: "Fetch HEAD", Ref: "HEAD"},
{Name: "Fetch Sha", Ref: fullSha},
}
for _, c := range refs {
t.Run(c.Name, func(t *testing.T) {
sha, err := cache.Fetch(ctx, cacheDir, remoteDir, c.Ref, "")
if !a.NoError(err) || !a.NotEmpty(sha) { //nolint:testifylint // pre-existing issue from nektos/act
return
}
atar, err := cache.GetTarArchive(ctx, cacheDir, sha, "js")
// NotNil, not NotEmpty: atar is a live io.PipeReader whose producer goroutine is
// writing concurrently; NotEmpty deep-reflects over its internals and races.
if !a.NoError(err) || !a.NotNil(atar) { //nolint:testifylint // pre-existing issue from nektos/act
return
}
// GetTarArchive streams from a background goroutine walking the shared repo.
// Drain and close so it finishes before the next subtest fetches into the same
// repo; otherwise the lingering walk races with that fetch.
defer func() {
_, _ = io.Copy(io.Discard, atar)
_ = atar.Close()
}()
mytar := tar.NewReader(atar)
th, err := mytar.Next()
if !a.NoError(err) || !a.NotEqual(0, th.Size) { //nolint:testifylint // pre-existing issue from nektos/act
return
}
buf := &bytes.Buffer{}
// G110: Potential DoS vulnerability via decompression bomb (gosec)
_, err = io.Copy(buf, mytar)
a.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
str := buf.String()
a.NotEmpty(str)
})
}
}
+2 -27
View File
@@ -181,20 +181,7 @@ func (rc *RunContext) compositeExecutor(action *model.Action) *compositeSteps {
stepPre := rc.newCompositeCommandExecutor(step.pre()) stepPre := rc.newCompositeCommandExecutor(step.pre())
preSteps = append(preSteps, newCompositeStepLogExecutor(stepPre, stepID)) preSteps = append(preSteps, newCompositeStepLogExecutor(stepPre, stepID))
steps = append(steps, func(ctx context.Context) error { steps = append(steps, newCompositeStepLogExecutor(rc.newCompositeCommandExecutor(step.main()), stepID))
ctx = WithCompositeStepLogger(ctx, stepID)
logger := common.Logger(ctx)
err := rc.newCompositeCommandExecutor(step.main())(ctx)
if err != nil {
logger.Errorf("##[error]%s", EscapeCommandData(err.Error()))
common.SetJobError(ctx, err)
} else if ctx.Err() != nil {
logger.Errorf("##[error]%s", EscapeCommandData(ctx.Err().Error()))
common.SetJobError(ctx, ctx.Err())
}
return nil
})
// run the post executor in reverse order // run the post executor in reverse order
if postExecutor != nil { if postExecutor != nil {
@@ -222,19 +209,7 @@ func (rc *RunContext) newCompositeCommandExecutor(executor common.Executor) comm
return func(ctx context.Context) error { return func(ctx context.Context) error {
ctx = WithCompositeLogger(ctx, &rc.Masks) ctx = WithCompositeLogger(ctx, &rc.Masks)
// We need to inject a composite RunContext related command logWriter := rc.commandLogWriter(ctx)
// handler into the current running job container
// We need this, to support scoping commands to the composite action
// executing.
rawLogger := common.Logger(ctx).WithField("raw_output", true)
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
if rc.Config.LogOutput {
rawLogger.Infof("%s", s)
} else {
rawLogger.Debugf("%s", s)
}
return true
})
oldout, olderr := rc.JobContainer.ReplaceLogWriter(logWriter, logWriter) oldout, olderr := rc.JobContainer.ReplaceLogWriter(logWriter, logWriter)
defer rc.JobContainer.ReplaceLogWriter(oldout, olderr) defer rc.JobContainer.ReplaceLogWriter(oldout, olderr)
+19 -69
View File
@@ -23,12 +23,10 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type closerMock struct { type closerFunc func()
mock.Mock
}
func (m *closerMock) Close() error { func (close closerFunc) Close() error {
m.Called() close()
return nil return nil
} }
@@ -39,6 +37,15 @@ runs:
using: 'node16' using: 'node16'
main: 'main.js' main: 'main.js'
`, "\t", " ") `, "\t", " ")
yamlAction := &model.Action{
Name: "name",
Runs: model.ActionRuns{
Using: "node16",
Main: "main.js",
PreIf: "always()",
PostIf: "always()",
},
}
table := []struct { table := []struct {
name string name string
@@ -52,30 +59,14 @@ runs:
step: &model.Step{}, step: &model.Step{},
filename: "action.yml", filename: "action.yml",
fileContent: yaml, fileContent: yaml,
expected: &model.Action{ expected: yamlAction,
Name: "name",
Runs: model.ActionRuns{
Using: "node16",
Main: "main.js",
PreIf: "always()",
PostIf: "always()",
},
},
}, },
{ {
name: "readActionYaml", name: "readActionYaml",
step: &model.Step{}, step: &model.Step{},
filename: "action.yaml", filename: "action.yaml",
fileContent: yaml, fileContent: yaml,
expected: &model.Action{ expected: yamlAction,
Name: "name",
Runs: model.ActionRuns{
Using: "node16",
Main: "main.js",
PreIf: "always()",
PostIf: "always()",
},
},
}, },
{ {
name: "readDockerfile", name: "readDockerfile",
@@ -121,14 +112,14 @@ runs:
for _, tt := range table { for _, tt := range table {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
closerMock := &closerMock{} closed := false
readFile := func(filename string) (io.Reader, io.Closer, error) { readFile := func(filename string) (io.Reader, io.Closer, error) {
if tt.filename != filename { if tt.filename != filename {
return nil, nil, fs.ErrNotExist return nil, nil, fs.ErrNotExist
} }
return strings.NewReader(tt.fileContent), closerMock, nil return strings.NewReader(tt.fileContent), closerFunc(func() { closed = true }), nil
} }
writeFile := func(filename string, data []byte, perm fs.FileMode) error { writeFile := func(filename string, data []byte, perm fs.FileMode) error {
@@ -137,58 +128,16 @@ runs:
return nil return nil
} }
if tt.filename != "" {
closerMock.On("Close")
}
action, err := readActionImpl(context.Background(), tt.step, "actionDir", "actionPath", readFile, writeFile) action, err := readActionImpl(context.Background(), tt.step, "actionDir", "actionPath", readFile, writeFile)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, tt.expected, action) assert.Equal(t, tt.expected, action)
closerMock.AssertExpectations(t) assert.Equal(t, tt.filename != "", closed)
}) })
} }
} }
// With AutoRemove the daemon reaps the container on exit, so act must not remove it afterwards.
func TestExecAsDockerAutoRemove(t *testing.T) {
orig := ContainerNewContainer
defer func() { ContainerNewContainer = orig }()
for _, tc := range []struct {
autoRemove bool
removes int
}{
{false, 2}, // stale + post-run
{true, 1}, // post-run skipped
} {
cm := &containerMock{}
ContainerNewContainer = func(*container.NewContainerInput) container.ExecutionsEnvironment { return cm }
step := &stepActionRemote{
Step: &model.Step{ID: "1", Uses: "org/action@v1"},
RunContext: &RunContext{
Config: &Config{AutoRemove: tc.autoRemove},
Run: &model.Run{JobID: "1", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}}},
JobContainer: cm,
},
action: &model.Action{Runs: model.ActionRuns{Using: "docker", Image: "docker://node:14"}},
}
removes := 0
cm.On("Pull", false).Return(func(context.Context) error { return nil })
cm.On("Remove").Return(func(context.Context) error { removes++; return nil })
cm.On("Create", []string(nil), []string(nil)).Return(func(context.Context) error { return nil })
cm.On("Start", true).Return(func(context.Context) error { return nil })
cm.On("Close").Return(func(context.Context) error { return nil })
require.NoError(t, execAsDocker(context.Background(), step, "action", t.TempDir(), t.TempDir(), false, stepStageMain))
cm.AssertExpectations(t)
assert.Equal(t, tc.removes, removes)
}
}
func TestActionRunner(t *testing.T) { func TestActionRunner(t *testing.T) {
table := []struct { table := []struct {
name string name string
@@ -337,11 +286,12 @@ func TestNewStepContainerDoesNotUseDockerSecrets(t *testing.T) {
step.On("getStepModel").Return(&model.Step{ID: "action"}) step.On("getStepModel").Return(&model.Step{ID: "action"})
step.On("getEnv").Return(&env) step.On("getEnv").Return(&env)
_ = newStepContainer(ctx, step, "registry.example.com/action:tag", nil, nil) _ = newStepContainer(ctx, step, "registry.example.com/action:tag", nil, nil, "")
// DOCKER_USERNAME/DOCKER_PASSWORD should not be injected as pull credentials for docker action containers. // DOCKER_USERNAME/DOCKER_PASSWORD should not be injected as pull credentials for docker action containers.
assert.Empty(t, captured.Username) assert.Empty(t, captured.Username)
assert.Empty(t, captured.Password) assert.Empty(t, captured.Password)
assert.True(t, captured.AutoRemove)
step.AssertExpectations(t) step.AssertExpectations(t)
} }
-4
View File
@@ -163,10 +163,6 @@ func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string,
logger := common.Logger(ctx) logger := common.Logger(ctx)
stepID := rc.CurrentStep stepID := rc.CurrentStep
outputName := kvPairs["name"] outputName := kvPairs["name"]
if outputMapping, ok := rc.OutputMappings[MappableOutput{StepID: stepID, OutputName: outputName}]; ok {
stepID = outputMapping.StepID
outputName = outputMapping.OutputName
}
result, ok := rc.StepResults[stepID] result, ok := rc.StepResults[stepID]
if !ok { if !ok {
+2 -5
View File
@@ -14,6 +14,8 @@ import (
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
) )
var noopExecutor = func(context.Context) error { return nil }
type containerMock struct { type containerMock struct {
mock.Mock mock.Mock
container.Container container.Container
@@ -50,11 +52,6 @@ func (cm *containerMock) UpdateFromEnv(srcPath string, env *map[string]string) c
return args.Get(0).(func(context.Context) error) return args.Get(0).(func(context.Context) error)
} }
func (cm *containerMock) UpdateFromImageEnv(env *map[string]string) common.Executor {
args := cm.Called(env)
return args.Get(0).(func(context.Context) error)
}
func (cm *containerMock) Copy(destPath string, files ...*container.FileEntry) common.Executor { func (cm *containerMock) Copy(destPath string, files ...*container.FileEntry) common.Executor {
args := cm.Called(destPath, files) args := cm.Called(destPath, files)
return args.Get(0).(func(context.Context) error) return args.Get(0).(func(context.Context) error)
+9 -15
View File
@@ -26,20 +26,12 @@ import (
"go.yaml.in/yaml/v4" "go.yaml.in/yaml/v4"
) )
// ExpressionEvaluator is the interface for evaluating expressions
type ExpressionEvaluator interface {
evaluate(context.Context, string, exprparser.DefaultStatusCheck) (any, error)
interpolate(context.Context, string) (string, error)
EvaluateYamlNode(context.Context, *yaml.Node) error
Interpolate(context.Context, string) string
}
// NewExpressionEvaluator creates a new evaluator // NewExpressionEvaluator creates a new evaluator
func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) ExpressionEvaluator { func (rc *RunContext) NewExpressionEvaluator(ctx context.Context) *ExpressionEvaluator {
return rc.NewExpressionEvaluatorWithEnv(ctx, rc.GetEnv()) return rc.NewExpressionEvaluatorWithEnv(ctx, rc.GetEnv())
} }
func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map[string]string) ExpressionEvaluator { func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map[string]string) *ExpressionEvaluator {
var workflowCallResult map[string]*model.WorkflowCallResult var workflowCallResult map[string]*model.WorkflowCallResult
// todo: cleanup EvaluationEnvironment creation // todo: cleanup EvaluationEnvironment creation
@@ -98,7 +90,7 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
HashFiles: getHashFilesFunction(ctx, rc), HashFiles: getHashFilesFunction(ctx, rc),
} }
ee.Runner = rc.getRunnerContext(ctx) ee.Runner = rc.getRunnerContext(ctx)
return expressionEvaluator{ return &expressionEvaluator{
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{ interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
Run: rc.Run, Run: rc.Run,
WorkingDir: rc.Config.Workdir, WorkingDir: rc.Config.Workdir,
@@ -111,7 +103,7 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
var hashfiles string var hashfiles string
// NewStepExpressionEvaluator creates a new evaluator // NewStepExpressionEvaluator creates a new evaluator
func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step) ExpressionEvaluator { func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step) *ExpressionEvaluator {
// todo: cleanup EvaluationEnvironment creation // todo: cleanup EvaluationEnvironment creation
job := rc.Run.Job() job := rc.Run.Job()
strategy := make(map[string]any) strategy := make(map[string]any)
@@ -150,7 +142,7 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step)
HashFiles: getHashFilesFunction(ctx, rc), HashFiles: getHashFilesFunction(ctx, rc),
} }
ee.Runner = rc.getRunnerContext(ctx) ee.Runner = rc.getRunnerContext(ctx)
return expressionEvaluator{ return &expressionEvaluator{
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{ interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
Run: rc.Run, Run: rc.Run,
WorkingDir: rc.Config.Workdir, WorkingDir: rc.Config.Workdir,
@@ -196,7 +188,7 @@ func getHashFilesFunction(ctx context.Context, rc *RunContext) func(v []reflect.
Mode: 0o644, Mode: 0o644,
Body: hashfiles, Body: hashfiles,
}). }).
Then(rc.execJobContainer([]string{"node", path.Join(rc.JobContainer.GetActPath(), name)}, Then(rc.JobContainer.Exec([]string{"node", path.Join(rc.JobContainer.GetActPath(), name)},
env, "", "")). env, "", "")).
Finally(func(context.Context) error { Finally(func(context.Context) error {
rc.JobContainer.ReplaceLogWriter(stdout, stderr) rc.JobContainer.ReplaceLogWriter(stdout, stderr)
@@ -222,6 +214,8 @@ type expressionEvaluator struct {
interpreter exprparser.Interpreter interpreter exprparser.Interpreter
} }
type ExpressionEvaluator = expressionEvaluator
func (ee expressionEvaluator) evaluate(ctx context.Context, in string, defaultStatusCheck exprparser.DefaultStatusCheck) (any, error) { func (ee expressionEvaluator) evaluate(ctx context.Context, in string, defaultStatusCheck exprparser.DefaultStatusCheck) (any, error) {
logger := common.Logger(ctx) logger := common.Logger(ctx)
logger.Debugf("evaluating expression '%s'", in) logger.Debugf("evaluating expression '%s'", in)
@@ -261,7 +255,7 @@ func (ee expressionEvaluator) interpolate(ctx context.Context, in string) (strin
// EvalBool evaluates an expression against given evaluator. An `if:` is an expression even without // EvalBool evaluates an expression against given evaluator. An `if:` is an expression even without
// `${{ }}`, while literal text around one makes the whole value a string. // `${{ }}`, while literal text around one makes the whole value a string.
func EvalBool(ctx context.Context, evaluator ExpressionEvaluator, expr string, defaultStatusCheck exprparser.DefaultStatusCheck) (bool, error) { func EvalBool(ctx context.Context, evaluator *expressionEvaluator, expr string, defaultStatusCheck exprparser.DefaultStatusCheck) (bool, error) {
return expreval.New(func(in string, dsc exprparser.DefaultStatusCheck) (any, error) { return expreval.New(func(in string, dsc exprparser.DefaultStatusCheck) (any, error) {
return evaluator.evaluate(ctx, in, dsc) return evaluator.evaluate(ctx, in, dsc)
}).EvalBool(expr, defaultStatusCheck) }).EvalBool(expr, defaultStatusCheck)
+1 -37
View File
@@ -240,44 +240,16 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
postExecutor = postExecutor.Finally(func(ctx context.Context) error { postExecutor = postExecutor.Finally(func(ctx context.Context) error {
jobError := common.JobError(ctx) jobError := common.JobError(ctx)
var err error var err error
// jobError == nil keeps a failed job's container alive for post-mortem debugging when
// AutoRemove is off (the act-CLI --rm behavior; the shipped runner always sets
// AutoRemove). A cancelled run is not a failure to inspect, and the cancel-path post
// context now carries its own error container so a failing post step makes jobError
// non-nil — OR in rc.jobCancelled so cancellation still always tears the container down.
if rc.Config.AutoRemove || jobError == nil || rc.jobCancelled {
// always allow 1 min for stopping and removing the runner, even if we were cancelled // always allow 1 min for stopping and removing the runner, even if we were cancelled
ctx, cancel := context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute) ctx, cancel := context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute)
defer cancel() defer cancel()
logger := common.Logger(ctx) logger := common.Logger(ctx)
tryUploadJobSummary(ctx, rc) tryUploadJobSummary(ctx, rc)
// For Gitea
// We don't need to call `stopServiceContainers` here since it will be called by following `info.stopContainer`
// logger.Infof("Cleaning up services for job %s", rc.JobName)
// if err := rc.stopServiceContainers()(ctx); err != nil {
// logger.Errorf("Error while cleaning services: %v", err)
// }
logger.Infof("Cleaning up container for job %s", rc.JobName) logger.Infof("Cleaning up container for job %s", rc.JobName)
if err = info.stopContainer()(ctx); err != nil { if err = info.stopContainer()(ctx); err != nil {
logger.Errorf("##[error]%s", EscapeCommandData("Error while stop job container: "+err.Error())) logger.Errorf("##[error]%s", EscapeCommandData("Error while stop job container: "+err.Error()))
} }
// For Gitea
// We don't need to call `NewDockerNetworkRemoveExecutor` here since it is called by above `info.stopContainer`
// if !rc.IsHostEnv(ctx) && rc.Config.ContainerNetworkMode == "" {
// // clean network in docker mode only
// // if the value of `ContainerNetworkMode` is empty string,
// // it means that the network to which containers are connecting is created by `runner`,
// // so, we should remove the network at last.
// networkName, _ := rc.networkName()
// logger.Infof("Cleaning up network for job %s, and network name is: %s", rc.JobName, networkName)
// if err := container.NewDockerNetworkRemoveExecutor(networkName)(ctx); err != nil {
// logger.Errorf("Error while cleaning network: %v", err)
// }
// }
}
setJobResult(ctx, info, rc, jobError == nil) setJobResult(ctx, info, rc, jobError == nil)
setJobOutputs(ctx, rc) setJobOutputs(ctx, rc)
@@ -651,15 +623,7 @@ func useStepLogger(rc *RunContext, stepModel *model.Step, stage stepStage, execu
return func(ctx context.Context) error { return func(ctx context.Context) error {
ctx = withStepLogger(ctx, stepModel.Number, stepModel.ID, rc.ExprEval.Interpolate(ctx, stepModel.String()), stage.String()) ctx = withStepLogger(ctx, stepModel.Number, stepModel.ID, rc.ExprEval.Interpolate(ctx, stepModel.String()), stage.String())
rawLogger := common.Logger(ctx).WithField("raw_output", true) logWriter := rc.commandLogWriter(ctx)
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
if rc.Config.LogOutput {
rawLogger.Infof("%s", s)
} else {
rawLogger.Debugf("%s", s)
}
return true
})
oldout, olderr := rc.JobContainer.ReplaceLogWriter(logWriter, logWriter) oldout, olderr := rc.JobContainer.ReplaceLogWriter(logWriter, logWriter)
defer rc.JobContainer.ReplaceLogWriter(oldout, olderr) defer rc.JobContainer.ReplaceLogWriter(oldout, olderr)
+4 -9
View File
@@ -336,6 +336,7 @@ func TestNewJobExecutor(t *testing.T) {
executedSteps: []string{ executedSteps: []string{
"startContainer", "startContainer",
"step1", "step1",
"stopContainer",
"interpolateOutputs", "interpolateOutputs",
"closeContainer", "closeContainer",
}, },
@@ -562,9 +563,8 @@ func TestNewJobExecutorRunsPostStepsAfterTimeout(t *testing.T) {
jim.On("startContainer").Return(func(ctx context.Context) error { return nil }) jim.On("startContainer").Return(func(ctx context.Context) error { return nil })
jim.On("interpolateOutputs").Return(func(ctx context.Context) error { return nil }) jim.On("interpolateOutputs").Return(func(ctx context.Context) error { return nil })
jim.On("closeContainer").Return(func(ctx context.Context) error { return nil }) jim.On("closeContainer").Return(func(ctx context.Context) error { return nil })
// The job timed out, so it must be reported as failed. stopContainer is left // The job timed out, so it must be reported as failed and still cleaned up.
// unexpected on purpose: a timed-out (failed) job preserves its error state, so jim.On("stopContainer").Return(func(context.Context) error { return nil })
// the graceful stop is skipped exactly like any other failure without AutoRemove.
jim.On("result", "failure") jim.On("result", "failure")
sm := &stepMock{} sm := &stepMock{}
@@ -984,12 +984,7 @@ func tarArchive(t *testing.T, entries ...tarEntry) []byte {
func newTestRC(wf *model.Workflow, matrix map[string]any) *RunContext { func newTestRC(wf *model.Workflow, matrix map[string]any) *RunContext {
return &RunContext{ return &RunContext{
Config: &Config{ Config: &Config{Workdir: ".", PlatformPicker: func([]string) string { return "ubuntu-latest" }},
Workdir: ".",
Platforms: map[string]string{
"ubuntu-latest": "ubuntu-latest",
},
},
StepResults: map[string]*model.StepResult{}, StepResults: map[string]*model.StepResult{},
Env: map[string]string{}, Env: map[string]string{},
Matrix: matrix, Matrix: matrix,
-95
View File
@@ -1,95 +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 (
"archive/tar"
"bytes"
"context"
"fmt"
"io"
"io/fs"
goURL "net/url"
"os"
"path/filepath"
"strings"
"gitea.com/gitea/runner/act/filecollector"
)
type LocalRepositoryCache struct {
Parent ActionCache
LocalRepositories map[string]string
CacheDirCache map[string]string
}
func (l *LocalRepositoryCache) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) {
if dest, ok := l.LocalRepositories[fmt.Sprintf("%s@%s", url, ref)]; ok {
l.CacheDirCache[fmt.Sprintf("%s@%s", cacheDir, ref)] = dest
return ref, nil
}
if purl, err := goURL.Parse(url); err == nil {
if dest, ok := l.LocalRepositories[fmt.Sprintf("%s@%s", strings.TrimPrefix(purl.Path, "/"), ref)]; ok {
l.CacheDirCache[fmt.Sprintf("%s@%s", cacheDir, ref)] = dest
return ref, nil
}
}
return l.Parent.Fetch(ctx, cacheDir, url, ref, token)
}
func (l *LocalRepositoryCache) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) {
// sha is mapped to ref in fetch if there is a local override
if dest, ok := l.CacheDirCache[fmt.Sprintf("%s@%s", cacheDir, sha)]; ok {
srcPath := filepath.Join(dest, includePrefix)
buf := &bytes.Buffer{}
tw := tar.NewWriter(buf)
defer tw.Close()
srcPath = filepath.Clean(srcPath)
fi, err := os.Lstat(srcPath)
if err != nil {
return nil, err
}
tc := &filecollector.TarCollector{
TarWriter: tw,
}
if fi.IsDir() {
srcPrefix := srcPath
if !strings.HasSuffix(srcPrefix, string(filepath.Separator)) {
srcPrefix += string(filepath.Separator)
}
fc := &filecollector.FileCollector{
Fs: &filecollector.DefaultFs{},
SrcPath: srcPath,
SrcPrefix: srcPrefix,
Handler: tc,
}
err = filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{}))
if err != nil {
return nil, err
}
} else {
var f io.ReadCloser
var linkname string
if fi.Mode()&fs.ModeSymlink != 0 {
linkname, err = os.Readlink(srcPath)
if err != nil {
return nil, err
}
} else {
f, err = os.Open(srcPath)
if err != nil {
return nil, err
}
defer f.Close()
}
err := tc.WriteFile(fi.Name(), fi, linkname, f)
if err != nil {
return nil, err
}
}
return io.NopCloser(buf), nil
}
return l.Parent.GetTarArchive(ctx, cacheDir, sha, includePrefix)
}
+3 -17
View File
@@ -99,10 +99,7 @@ func WithJobLogger(ctx context.Context, jobID, jobName string, config *Config, m
mux.Lock() mux.Lock()
defer mux.Unlock() defer mux.Unlock()
nextColor++ nextColor++
formatter = &jobLogFormatter{ formatter = &jobLogFormatter{color: colors[nextColor%len(colors)]}
color: colors[nextColor%len(colors)],
logPrefixJobID: config.LogPrefixJobID,
}
} }
logger = logrus.New() logger = logrus.New()
@@ -339,7 +336,6 @@ func (f *maskedFormatter) Format(entry *logrus.Entry) ([]byte, error) {
type jobLogFormatter struct { type jobLogFormatter struct {
color int color int
logPrefixJobID bool
} }
func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) { func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
@@ -363,12 +359,7 @@ func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) { func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
entry.Message = strings.TrimSuffix(entry.Message, "\n") entry.Message = strings.TrimSuffix(entry.Message, "\n")
var job any job := entry.Data["job"]
if f.logPrefixJobID {
job = entry.Data["jobID"]
} else {
job = entry.Data["job"]
}
debugFlag := "" debugFlag := ""
if entry.Level == logrus.DebugLevel { if entry.Level == logrus.DebugLevel {
@@ -391,12 +382,7 @@ func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) { func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
entry.Message = strings.TrimSuffix(entry.Message, "\n") entry.Message = strings.TrimSuffix(entry.Message, "\n")
var job any job := entry.Data["job"]
if f.logPrefixJobID {
job = entry.Data["jobID"]
} else {
job = entry.Data["job"]
}
debugFlag := "" debugFlag := ""
if entry.Level == logrus.DebugLevel { if entry.Level == logrus.DebugLevel {
+7 -58
View File
@@ -5,7 +5,6 @@
package runner package runner
import ( import (
"archive/tar"
"context" "context"
"fmt" "fmt"
"net/url" "net/url"
@@ -78,10 +77,6 @@ func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor {
filename := fmt.Sprintf("%s/%s@%s", remoteReusableWorkflow.Org, remoteReusableWorkflow.Repo, remoteReusableWorkflow.Ref) filename := fmt.Sprintf("%s/%s@%s", remoteReusableWorkflow.Org, remoteReusableWorkflow.Repo, remoteReusableWorkflow.Ref)
workflowDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(filename)) workflowDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(filename))
if rc.Config.ActionCache != nil {
return newActionCacheReusableWorkflowExecutor(rc, filename, remoteReusableWorkflow)
}
token := getGitCloneToken(rc.Config, remoteReusableWorkflow.CloneURL()) token := getGitCloneToken(rc.Config, remoteReusableWorkflow.CloneURL())
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
@@ -90,41 +85,6 @@ func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor {
) )
} }
func newActionCacheReusableWorkflowExecutor(rc *RunContext, filename string, remoteReusableWorkflow *remoteReusableWorkflow) common.Executor {
return func(ctx context.Context) error {
ghctx := rc.getGithubContext(ctx)
remoteReusableWorkflow.URL = ghctx.ServerURL
sha, err := rc.Config.ActionCache.Fetch(ctx, filename, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, ghctx.Token)
if err != nil {
return err
}
archive, err := rc.Config.ActionCache.GetTarArchive(ctx, filename, sha, ".github/workflows/"+remoteReusableWorkflow.Filename)
if err != nil {
return err
}
defer archive.Close()
treader := tar.NewReader(archive)
if _, err = treader.Next(); err != nil {
return err
}
planner, err := model.NewSingleWorkflowPlanner(remoteReusableWorkflow.Filename, treader)
if err != nil {
return err
}
plan, err := planner.PlanEvent("workflow_call")
if err != nil {
return err
}
runner, err := NewReusableWorkflowRunner(rc)
if err != nil {
return err
}
return runner.NewPlanExecutor(plan)(ctx)
}
}
// cloneRemoteReusableWorkflow always invokes the clone executor — moving refs // cloneRemoteReusableWorkflow always invokes the clone executor — moving refs
// (branches, tags) must be re-resolved each run, matching GitHub Actions. // (branches, tags) must be re-resolved each run, matching GitHub Actions.
// //
@@ -147,15 +107,12 @@ 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 {
// Scoped to the yaml read so concurrent invocations don't serialize // Serialize workflow reads with cache updates.
// on the whole job run.
planner, err := func() (model.WorkflowPlanner, error) { planner, err := func() (model.WorkflowPlanner, error) {
defer git.AcquireCloneLock(directory)() defer git.AcquireCloneLock(directory)()
return modelNewWorkflowPlanner(path.Join(directory, workflow), true) return model.NewWorkflowPlanner(path.Join(directory, workflow), true)
}() }()
if err != nil { if err != nil {
return err return err
@@ -166,12 +123,11 @@ func newReusableWorkflowExecutor(rc *RunContext, directory, workflow string) com
return err return err
} }
runner, err := NewReusableWorkflowRunner(rc) runner, err := newReusableWorkflowRunner(rc)
if err != nil { if err != nil {
return err return err
} }
// return runner.NewPlanExecutor(plan)(ctx)
return common.NewPipelineExecutor( // For Gitea return common.NewPipelineExecutor( // For Gitea
runner.NewPlanExecutor(plan), runner.NewPlanExecutor(plan),
setReusedWorkflowCallerResult(rc, runner), setReusedWorkflowCallerResult(rc, runner),
@@ -179,7 +135,7 @@ func newReusableWorkflowExecutor(rc *RunContext, directory, workflow string) com
} }
} }
func NewReusableWorkflowRunner(rc *RunContext) (Runner, error) { func newReusableWorkflowRunner(rc *RunContext) (*runnerImpl, error) {
runner := &runnerImpl{ runner := &runnerImpl{
config: rc.Config, config: rc.Config,
eventJSON: rc.EventJSON, eventJSON: rc.EventJSON,
@@ -255,16 +211,9 @@ func newRemoteReusableWorkflowFromAbsoluteURL(uses string) *remoteReusableWorkfl
} }
// For Gitea // For Gitea
func setReusedWorkflowCallerResult(rc *RunContext, runner Runner) common.Executor { func setReusedWorkflowCallerResult(rc *RunContext, runner *runnerImpl) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) caller := runner.caller
runnerImpl, ok := runner.(*runnerImpl)
if !ok {
logger.Warn("Failed to get caller from runner")
return nil
}
caller := runnerImpl.caller
allJobDone := true allJobDone := true
hasFailure := false hasFailure := false
@@ -294,7 +243,7 @@ func setReusedWorkflowCallerResult(rc *RunContext, runner Runner) common.Executo
unlock := lockJob(rc.Run.Job()) unlock := lockJob(rc.Run.Job())
rc.result(reusedWorkflowJobResult) rc.result(reusedWorkflowJobResult)
unlock() unlock()
logger.WithField("jobResult", reusedWorkflowJobResult).Infof("Job %s", reusedWorkflowJobResultMessage) common.Logger(ctx).WithField("jobResult", reusedWorkflowJobResult).Infof("Job %s", reusedWorkflowJobResultMessage)
} }
} }
+3 -20
View File
@@ -5,7 +5,6 @@ package runner
import ( import (
"context" "context"
"errors"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -77,19 +76,11 @@ func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
func TestNewReusableWorkflowExecutorHoldsCloneLock(t *testing.T) { func TestNewReusableWorkflowExecutorHoldsCloneLock(t *testing.T) {
workflowDir := t.TempDir() workflowDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(workflowDir, "reusable.yml"), []byte(":"), 0o644))
unlockOnce := sync.OnceFunc(git.AcquireCloneLock(workflowDir)) unlockOnce := sync.OnceFunc(git.AcquireCloneLock(workflowDir))
defer unlockOnce() 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{ rc := &RunContext{
Config: &Config{}, Config: &Config{},
Run: &model.Run{Workflow: &model.Workflow{Jobs: map[string]*model.Job{}}}, Run: &model.Run{Workflow: &model.Workflow{Jobs: map[string]*model.Job{}}},
@@ -100,26 +91,18 @@ func TestNewReusableWorkflowExecutorHoldsCloneLock(t *testing.T) {
go func() { done <- exec(context.Background()) }() go func() { done <- exec(context.Background()) }()
select { select {
case <-plannerCalled:
t.Fatal("planner ran while clone lock was held")
case err := <-done: case err := <-done:
t.Fatalf("executor returned before planner was reached: %v", err) t.Fatalf("executor returned while clone lock was held: %v", err)
case <-time.After(50 * time.Millisecond): case <-time.After(50 * time.Millisecond):
} }
unlockOnce() unlockOnce()
select {
case <-plannerCalled:
case <-time.After(time.Second):
t.Fatal("planner not called after lock was released")
}
select { select {
case err := <-done: case err := <-done:
require.Error(t, err) require.Error(t, err)
case <-time.After(time.Second): case <-time.After(time.Second):
t.Fatal("executor did not return after planner ran") t.Fatal("executor did not return after lock was released")
} }
} }
+33 -87
View File
@@ -56,10 +56,9 @@ type RunContext struct {
CurrentStepIndex int CurrentStepIndex int
StepResults map[string]*model.StepResult StepResults map[string]*model.StepResult
IntraActionState map[string]map[string]string IntraActionState map[string]map[string]string
ExprEval ExpressionEvaluator ExprEval *expressionEvaluator
JobContainer container.ExecutionsEnvironment JobContainer container.ExecutionsEnvironment
serviceContainers []*serviceContainer serviceContainers []*serviceContainer
OutputMappings map[MappableOutput]MappableOutput
JobName string JobName string
ActionPath string ActionPath string
Parent *RunContext Parent *RunContext
@@ -148,11 +147,6 @@ func (rc *RunContext) AddMask(mask string) {
rc.Masks = append(rc.Masks, mask) rc.Masks = append(rc.Masks, mask)
} }
type MappableOutput struct {
StepID string
OutputName string
}
func (rc *RunContext) String() string { func (rc *RunContext) String() string {
name := fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name) name := fmt.Sprintf("%s/%s", rc.Run.Workflow.Name, rc.Name)
if rc.caller != nil { if rc.caller != nil {
@@ -341,16 +335,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) logWriter := rc.commandLogWriter(ctx)
rawLogger := logger.WithField(rawOutputField, true)
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
if rc.Config.LogOutput {
rawLogger.Infof("%s", s)
} else {
rawLogger.Debugf("%s", s)
}
return true
})
cacheDir := rc.ActionCacheDir() cacheDir := rc.ActionCacheDir()
randBytes := make([]byte, 8) randBytes := make([]byte, 8)
_, _ = rand.Read(randBytes) _, _ = rand.Read(randBytes)
@@ -437,15 +422,7 @@ 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(rawOutputField, true) logWriter := rc.commandLogWriter(ctx)
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
if rc.Config.LogOutput {
rawLogger.Infof("%s", s)
} else {
rawLogger.Debugf("%s", s)
}
return true
})
username, password, err := rc.handleCredentials(ctx) username, password, err := rc.handleCredentials(ctx)
if err != nil { if err != nil {
@@ -497,7 +474,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
} }
// keep these local: reusing username/password would overwrite the // keep these local: reusing username/password would overwrite the
// credentials the job container is pulled with further down // credentials the job container is pulled with further down
serviceUsername, servicePassword, err := rc.handleServiceCredentials(ctx, spec.Credentials) serviceUsername, servicePassword, err := rc.interpolateCredentials(ctx, spec.Credentials, "")
if err != nil { if err != nil {
return fmt.Errorf("failed to handle service %s credentials: %w", serviceID, err) return fmt.Errorf("failed to handle service %s credentials: %w", serviceID, err)
} }
@@ -568,7 +545,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
UsernsMode: rc.Config.UsernsMode, UsernsMode: rc.Config.UsernsMode,
Platform: rc.Config.ContainerArchitecture, Platform: rc.Config.ContainerArchitecture,
Options: rc.options(ctx), Options: rc.options(ctx),
AutoRemove: rc.Config.AutoRemove, AutoRemove: true,
ValidVolumes: rc.validVolumes(), ValidVolumes: rc.validVolumes(),
AllocatePTY: rc.Config.AllocatePTY, AllocatePTY: rc.Config.AllocatePTY,
}) })
@@ -604,12 +581,20 @@ func (rc *RunContext) startJobContainer() common.Executor {
} }
} }
func (rc *RunContext) commandLogWriter(ctx context.Context) io.Writer {
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
return common.NewLineWriter(rc.commandHandler(ctx), func(line string) bool {
rawLogger.Infof("%s", line)
return true
})
}
// cleanupJobResources removes everything the job created, continuing past failures. // cleanupJobResources removes everything the job created, continuing past failures.
// Only job container and volume errors are returned, the rest are logged. // Only job container and volume errors are returned, the rest are logged.
func (rc *RunContext) cleanupJobResources(networkName string, createAndDeleteNetwork bool) common.Executor { func (rc *RunContext) cleanupJobResources(networkName string, createAndDeleteNetwork bool) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
logger := common.Logger(ctx) logger := common.Logger(ctx)
removeJobContainer := rc.JobContainer != nil && !rc.Config.ReuseContainers removeJobContainer := rc.JobContainer != nil
var errs []error var errs []error
if removeJobContainer { if removeJobContainer {
@@ -639,12 +624,6 @@ func (rc *RunContext) cleanupJobResources(networkName string, createAndDeleteNet
} }
} }
func (rc *RunContext) execJobContainer(cmd []string, env map[string]string, user, workdir string) common.Executor { //nolint:unparam // pre-existing issue from nektos/act
return func(ctx context.Context) error {
return rc.JobContainer.Exec(cmd, env, user, workdir)(ctx)
}
}
func (rc *RunContext) ApplyExtraPath(ctx context.Context, env *map[string]string) { func (rc *RunContext) ApplyExtraPath(ctx context.Context, env *map[string]string) {
if len(rc.ExtraPath) > 0 { if len(rc.ExtraPath) > 0 {
path := rc.JobContainer.GetPathVariableName() path := rc.JobContainer.GetPathVariableName()
@@ -1078,19 +1057,9 @@ func (rc *RunContext) runsOnImage(ctx context.Context) string {
runsOn[i] = rc.ExprEval.Interpolate(ctx, v) runsOn[i] = rc.ExprEval.Interpolate(ctx, v)
} }
if pick := rc.Config.PlatformPicker; pick != nil { if rc.Config.PlatformPicker != nil {
if image := pick(runsOn); image != "" { return rc.Config.PlatformPicker(runsOn)
return image
} }
}
for _, platformName := range rc.runsOnPlatformNames(ctx) {
image := rc.Config.Platforms[strings.ToLower(platformName)]
if image != "" {
return image
}
}
return "" return ""
} }
@@ -1367,9 +1336,9 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext
ghc.SetBaseAndHeadRef() ghc.SetBaseAndHeadRef()
repoPath := rc.Config.Workdir repoPath := rc.Config.Workdir
ghcontext.SetRepositoryAndOwner(ctx, ghc, rc.Config.GitHubInstance, rc.Config.RemoteName, repoPath) ghcontext.SetRepositoryAndOwner(ctx, ghc, rc.Config.GitHubInstance, repoPath)
if ghc.Ref == "" { if ghc.Ref == "" {
ghcontext.SetRef(ctx, ghc, rc.Config.DefaultBranch, repoPath) ghcontext.SetRef(ctx, ghc, repoPath)
} }
if ghc.Sha == "" { if ghc.Sha == "" {
ghcontext.SetSha(ctx, ghc, repoPath) ghcontext.SetSha(ctx, ghc, repoPath)
@@ -1585,53 +1554,30 @@ func (rc *RunContext) handleCredentials(ctx context.Context) (string, string, er
return "", "", nil return "", "", nil
} }
if len(container.Credentials) != 2 { return rc.interpolateCredentials(ctx, container.Credentials, "container.")
err := errors.New("invalid property count for key 'credentials:'") }
return "", "", err
func (rc *RunContext) interpolateCredentials(ctx context.Context, credentials map[string]string, prefix string) (string, string, error) {
if credentials == nil {
return "", "", nil
}
if len(credentials) != 2 {
return "", "", errors.New("invalid property count for key 'credentials:'")
} }
ee := rc.NewExpressionEvaluator(ctx) ee := rc.NewExpressionEvaluator(ctx)
var username, password string username := ee.Interpolate(ctx, credentials["username"])
if username = ee.Interpolate(ctx, container.Credentials["username"]); username == "" { if username == "" {
err := errors.New("failed to interpolate container.credentials.username") return "", "", errors.New("failed to interpolate " + prefix + "credentials.username")
return "", "", err
} }
if password = ee.Interpolate(ctx, container.Credentials["password"]); password == "" { password := ee.Interpolate(ctx, credentials["password"])
err := errors.New("failed to interpolate container.credentials.password") if password == "" {
return "", "", err return "", "", errors.New("failed to interpolate " + prefix + "credentials.password")
}
if container.Credentials["username"] == "" || container.Credentials["password"] == "" {
err := errors.New("container.credentials cannot be empty")
return "", "", err
} }
return username, password, nil return username, password, nil
} }
func (rc *RunContext) handleServiceCredentials(ctx context.Context, creds map[string]string) (username, password string, err error) {
if creds == nil {
return username, password, err
}
if len(creds) != 2 {
err = errors.New("invalid property count for key 'credentials:'")
return username, password, err
}
ee := rc.NewExpressionEvaluator(ctx)
if username = ee.Interpolate(ctx, creds["username"]); username == "" {
err = errors.New("failed to interpolate credentials.username")
return username, password, err
}
if password = ee.Interpolate(ctx, creds["password"]); password == "" {
err = errors.New("failed to interpolate credentials.password")
return username, password, err
}
return username, password, err
}
// GetServiceBindsAndMounts returns the binds and mounts for the service container, resolving paths as appopriate // GetServiceBindsAndMounts returns the binds and mounts for the service container, resolving paths as appopriate
func (rc *RunContext) GetServiceBindsAndMounts(svcVolumes []string) ([]string, map[string]string) { func (rc *RunContext) GetServiceBindsAndMounts(svcVolumes []string) ([]string, map[string]string) {
binds, mounts, claimed := splitVolumes(svcVolumes) binds, mounts, claimed := splitVolumes(svcVolumes)
+7 -17
View File
@@ -271,10 +271,7 @@ jobs:
Name: "test", Name: "test",
Config: &Config{ Config: &Config{
Workdir: "/tmp", Workdir: "/tmp",
// no daemon: an explicit network mode creates no network, and
// reusing containers short-circuits the volume cleanup executors
ContainerNetworkMode: "host", ContainerNetworkMode: "host",
ReuseContainers: true,
Env: map[string]string{}, Env: map[string]string{},
Secrets: map[string]string{}, Secrets: map[string]string{},
}, },
@@ -286,7 +283,8 @@ jobs:
} }
rc.ExprEval = rc.NewExpressionEvaluator(t.Context()) rc.ExprEval = rc.NewExpressionEvaluator(t.Context())
require.NoError(t, rc.startJobContainer()(t.Context())) t.Setenv("DOCKER_HOST", "unix:///nonexistent.sock")
require.Error(t, rc.startJobContainer()(t.Context()))
credentials := map[string][2]string{} credentials := map[string][2]string{}
for _, in := range inputs { for _, in := range inputs {
@@ -335,7 +333,6 @@ jobs:
Config: &Config{ Config: &Config{
Workdir: "/tmp", Workdir: "/tmp",
ContainerNetworkMode: "host", ContainerNetworkMode: "host",
ReuseContainers: true,
Env: map[string]string{}, Env: map[string]string{},
ProxyEnv: map[string]string{"http_proxy": "http://proxy:3128", "no_proxy": "internal.example"}, ProxyEnv: map[string]string{"http_proxy": "http://proxy:3128", "no_proxy": "internal.example"},
Secrets: map[string]string{}, Secrets: map[string]string{},
@@ -348,7 +345,8 @@ jobs:
} }
rc.ExprEval = rc.NewExpressionEvaluator(t.Context()) rc.ExprEval = rc.NewExpressionEvaluator(t.Context())
require.NoError(t, rc.startJobContainer()(t.Context())) t.Setenv("DOCKER_HOST", "unix:///nonexistent.sock")
require.Error(t, rc.startJobContainer()(t.Context()))
env := map[string][]string{} env := map[string][]string{}
for _, in := range inputs { for _, in := range inputs {
@@ -668,7 +666,6 @@ func TestGetGitHubContext(t *testing.T) {
Env: map[string]string{}, Env: map[string]string{},
ExtraPath: []string{}, ExtraPath: []string{},
StepResults: map[string]*model.StepResult{}, StepResults: map[string]*model.StepResult{},
OutputMappings: map[MappableOutput]MappableOutput{},
} }
rc.Run.JobID = "job1" rc.Run.JobID = "job1"
@@ -745,12 +742,7 @@ func TestGetGithubContextRef(t *testing.T) {
func createIfTestRunContext(jobs map[string]*model.Job) *RunContext { func createIfTestRunContext(jobs map[string]*model.Job) *RunContext {
rc := &RunContext{ rc := &RunContext{
Config: &Config{ Config: &Config{Workdir: ".", PlatformPicker: func([]string) string { return "ubuntu-latest" }},
Workdir: ".",
Platforms: map[string]string{
"ubuntu-latest": "ubuntu-latest",
},
},
Env: map[string]string{}, Env: map[string]string{},
Run: &model.Run{ Run: &model.Run{
JobID: "job1", JobID: "job1",
@@ -1304,15 +1296,13 @@ func TestRunContextImageOS(t *testing.T) {
t.Run("prefers the release in the resolved image tag", func(t *testing.T) { t.Run("prefers the release in the resolved image tag", func(t *testing.T) {
rc := createRunsOnRunContext(t, "ubuntu-latest") rc := createRunsOnRunContext(t, "ubuntu-latest")
rc.Config.Platforms = map[string]string{ rc.Config.PlatformPicker = func([]string) string { return "docker.gitea.com/runner-images:ubuntu-24.04" }
"ubuntu-latest": "docker.gitea.com/runner-images:ubuntu-24.04",
}
assert.Equal(t, "ubuntu24", rc.imageOS(ctx)) assert.Equal(t, "ubuntu24", rc.imageOS(ctx))
}) })
t.Run("falls back to the runs-on label", func(t *testing.T) { t.Run("falls back to the runs-on label", func(t *testing.T) {
rc := createRunsOnRunContext(t, "ubuntu-22.04") rc := createRunsOnRunContext(t, "ubuntu-22.04")
rc.Config.Platforms = map[string]string{"ubuntu-22.04": "some-image"} rc.Config.PlatformPicker = func([]string) string { return "some-image" }
assert.Equal(t, "ubuntu22", rc.imageOS(ctx)) assert.Equal(t, "ubuntu22", rc.imageOS(ctx))
}) })
+10 -59
View File
@@ -6,7 +6,6 @@ package runner
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"maps" "maps"
"os" "os"
@@ -22,35 +21,24 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// Runner provides capabilities to run GitHub actions
type Runner interface {
NewPlanExecutor(plan *model.Plan) common.Executor
}
// Config contains the config for a new runner // Config contains the config for a new runner
type Config struct { type Config struct {
Actor string // the user that triggered the event Actor string // the user that triggered the event
Workdir string // path to working directory Workdir string // path to working directory
ActionCacheDir string // path used for caching action contents ActionCacheDir string // path used for caching action contents
ActionOfflineMode bool // when offline, use cached action contents ActionOfflineMode bool // when offline, use cached action contents
ActionCloneDepth int // limit history when cloning an action repo; 0 clones every branch in full ActionCloneDepth int // limit history when cloning an action repo, 0 clones every branch in full
BindWorkdir bool // bind the workdir to the job container BindWorkdir bool // bind the workdir to the job container
EventName string // name of event to run EventName string // name of event to run
EventPath string // path to JSON file to use for event.json in containers EventPath string // path to JSON file to use for event.json in containers
DefaultBranch string // name of the main branch for this repository
ReuseContainers bool // reuse containers to maintain state
ForcePull bool // force pulling of the image, even if already present ForcePull bool // force pulling of the image, even if already present
ForceRebuild bool // force rebuilding local docker image action ForceRebuild bool // force rebuilding local docker image action
LogOutput bool // log the output from docker run
JSONLogger bool // use json or text logger JSONLogger bool // use json or text logger
LogPrefixJobID bool // switches from the full job name to the job id
Env map[string]string // env for containers Env map[string]string // env for containers
Inputs map[string]string // manually passed action inputs
Secrets map[string]string // list of secrets Secrets map[string]string // list of secrets
Vars map[string]string // list of vars Vars map[string]string // list of vars
Token string // GitHub token Token string // GitHub token
InsecureSecrets bool // switch hiding output when printing to terminal InsecureSecrets bool // switch hiding output when printing to terminal
Platforms map[string]string // list of platforms
Privileged bool // use privileged mode Privileged bool // use privileged mode
UsernsMode string // user namespace to use UsernsMode string // user namespace to use
ContainerArchitecture string // Desired OS/architecture platform for running containers ContainerArchitecture string // Desired OS/architecture platform for running containers
@@ -60,23 +48,17 @@ type Config struct {
GitHubInstance string // GitHub instance to use, default "github.com" GitHubInstance string // GitHub instance to use, default "github.com"
ContainerCapAdd []string // list of kernel capabilities to add to the containers ContainerCapAdd []string // list of kernel capabilities to add to the containers
ContainerCapDrop []string // list of kernel capabilities to remove from the containers ContainerCapDrop []string // list of kernel capabilities to remove from the containers
AutoRemove bool // controls if the container is automatically removed upon workflow completion
ArtifactServerPath string // the path where the artifact server stores uploads ArtifactServerPath string // the path where the artifact server stores uploads
ArtifactServerAddr string // the address the artifact server binds to ArtifactServerAddr string // the address the artifact server binds to
ArtifactServerPort string // the port the artifact server binds to ArtifactServerPort string // the port the artifact server binds to
NoSkipCheckout bool // do not skip actions/checkout NoSkipCheckout bool // do not skip actions/checkout
DisableActEnv bool // do not inject the ACT=true environment variable into jobs DisableActEnv bool // do not inject the ACT=true environment variable into jobs
RemoteName string // remote name in local git repo config
ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub
ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub.
Matrix map[string]map[string]bool // Matrix config to run
ContainerNetworkMode docker_container.NetworkMode // the network mode of job containers (the value of --network) ContainerNetworkMode docker_container.NetworkMode // the network mode of job containers (the value of --network)
ContainerNetworkCreateOptions container.NewDockerNetworkCreateExecutorInput // the default network create options ContainerNetworkCreateOptions container.NewDockerNetworkCreateExecutorInput // the default network create options
ActionCache ActionCache // Use a custom ActionCache Implementation
ProxyEnv map[string]string // the proxy variables the job runs with, also given to service containers and image builds ProxyEnv map[string]string // the proxy variables the job runs with, also given to service containers and image builds
NoActionPatch bool // run actions exactly as published, applying no compatibility patches, see patch_actions.go NoActionPatch bool // run actions exactly as published, applying no compatibility patches, see patch_actions.go
PresetGitHubContext *model.GithubContext // the preset github context, overrides some fields like DefaultBranch, Env, Secrets etc. PresetGitHubContext *model.GithubContext // overrides actor, ref, repository, token and related context fields
EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath
ContainerNamePrefix string // the prefix of container name ContainerNamePrefix string // the prefix of container name
ContainerMaxLifetime time.Duration // the max lifetime of job containers ContainerMaxLifetime time.Duration // the max lifetime of job containers
@@ -88,7 +70,7 @@ type Config struct {
// differ from GitHubInstance when the runner registered with a different hostname than // differ from GitHubInstance when the runner registered with a different hostname than
// AppURL. It is never set for github.com or a GithubMirror, so the token stays on-instance. // AppURL. It is never set for github.com or a GithubMirror, so the token stays on-instance.
DefaultActionInstanceIsSelfHosted bool DefaultActionInstanceIsSelfHosted bool
PlatformPicker func(labels []string) string // platform picker, it will take precedence over Platforms if isn't nil PlatformPicker func(labels []string) string
JobLoggerLevel *log.Level // the level of job logger JobLoggerLevel *log.Level // the level of job logger
ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers
SharedToolCache bool // one tool cache for all jobs instead of one per job SharedToolCache bool // one tool cache for all jobs instead of one per job
@@ -141,8 +123,10 @@ type runnerImpl struct {
caller *caller // the job calling this runner (caller of a reusable workflow) caller *caller // the job calling this runner (caller of a reusable workflow)
} }
type Runner = runnerImpl
// New Creates a new Runner // New Creates a new Runner
func New(runnerConfig *Config) (Runner, error) { func New(runnerConfig *Config) (*Runner, error) {
runner := &runnerImpl{ runner := &runnerImpl{
config: runnerConfig, config: runnerConfig,
} }
@@ -150,7 +134,7 @@ func New(runnerConfig *Config) (Runner, error) {
return runner.configure() return runner.configure()
} }
func (runner *runnerImpl) configure() (Runner, error) { func (runner *runnerImpl) configure() (*runnerImpl, error) {
if runner.config.RunnerName == "" { if runner.config.RunnerName == "" {
// Callers that do not register, such as `exec`, still get a `runner.name`. // Callers that do not register, such as `exec`, still get a `runner.name`.
runner.config.RunnerName, _ = os.Hostname() runner.config.RunnerName, _ = os.Hostname()
@@ -166,15 +150,6 @@ func (runner *runnerImpl) configure() (Runner, error) {
return nil, err return nil, err
} }
runner.eventJSON = string(eventJSONBytes) runner.eventJSON = string(eventJSONBytes)
} else if len(runner.config.Inputs) != 0 {
eventMap := map[string]map[string]string{
"inputs": runner.config.Inputs,
}
eventJSON, err := json.Marshal(eventMap)
if err != nil {
return nil, err
}
runner.eventJSON = string(eventJSON)
} }
return runner, nil return runner, nil
} }
@@ -213,7 +188,6 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
log.Debugf("Job.Outputs: %v", job.Outputs) log.Debugf("Job.Outputs: %v", job.Outputs)
log.Debugf("Job.Uses: %v", job.Uses) log.Debugf("Job.Uses: %v", job.Uses)
log.Debugf("Job.With: %v", job.With) log.Debugf("Job.With: %v", job.With)
// log.Debugf("Job.RawSecrets: %v", job.RawSecrets)
log.Debugf("Job.Result: %v", job.Result) log.Debugf("Job.Result: %v", job.Result)
if job.Strategy != nil { if job.Strategy != nil {
@@ -231,15 +205,11 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
} }
} }
var matrixes []map[string]any matrixes, err := job.GetMatrixes()
if m, err := job.GetMatrixes(); err != nil { if err != nil {
log.Errorf("Error while get job's matrix: %v", err) log.Errorf("Error while get job's matrix: %v", err)
} else {
log.Debugf("Job Matrices: %v", m)
log.Debugf("Runner Matrices: %v", runner.config.Matrix)
matrixes = selectMatrixes(m, runner.config.Matrix)
} }
log.Debugf("Final matrix after applying user inclusions '%v'", matrixes) log.Debugf("Job Matrices: %v", matrixes)
maxParallel := 4 maxParallel := 4
if job.Strategy != nil { if job.Strategy != nil {
@@ -344,25 +314,6 @@ func handleFailure(plan *model.Plan) common.Executor {
} }
} }
func selectMatrixes(originalMatrixes []map[string]any, targetMatrixValues map[string]map[string]bool) []map[string]any {
matrixes := make([]map[string]any, 0)
for _, original := range originalMatrixes {
flag := true
for key, val := range original {
if allowedVals, ok := targetMatrixValues[key]; ok {
valToString := fmt.Sprintf("%v", val)
if _, ok := allowedVals[valToString]; !ok {
flag = false
}
}
}
if flag {
matrixes = append(matrixes, original)
}
}
return matrixes
}
func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, matrix map[string]any) *RunContext { func (runner *runnerImpl) newRunContext(ctx context.Context, run *model.Run, matrix map[string]any) *RunContext {
rc := &RunContext{ rc := &RunContext{
Config: runner.config, Config: runner.config,
+13 -81
View File
@@ -10,7 +10,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"slices" "slices"
@@ -24,7 +23,6 @@ import (
"github.com/joho/godotenv" "github.com/joho/godotenv"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
assert "github.com/stretchr/testify/assert" assert "github.com/stretchr/testify/assert"
"go.yaml.in/yaml/v4"
) )
var ( var (
@@ -35,6 +33,17 @@ var (
secrets map[string]string secrets map[string]string
) )
func mapPlatformPicker(platforms map[string]string) func([]string) string {
return func(labels []string) string {
for _, label := range labels {
if image := platforms[strings.ToLower(label)]; image != "" {
return image
}
}
return ""
}
}
func init() { func init() {
if p := os.Getenv("ACT_TEST_IMAGE"); p != "" { if p := os.Getenv("ACT_TEST_IMAGE"); p != "" {
baseImage = p baseImage = p
@@ -193,25 +202,18 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
BindWorkdir: false, BindWorkdir: false,
EventName: j.eventName, EventName: j.eventName,
EventPath: cfg.EventPath, EventPath: cfg.EventPath,
Platforms: j.platforms, PlatformPicker: mapPlatformPicker(j.platforms),
// fixtures reuse workflow and job names, so parallel tests would collide without this // fixtures reuse workflow and job names, so parallel tests would collide without this
ContainerNamePrefix: strings.ReplaceAll(t.Name(), "/", "-"), ContainerNamePrefix: strings.ReplaceAll(t.Name(), "/", "-"),
ReuseContainers: false,
// as the shipped runner does, else a fixture asserting a job failure keeps its
// container, and its network, on the daemon forever
AutoRemove: true,
// 0 would run jobs runtime.NumCPU()-wide, making the network peak machine-dependent // 0 would run jobs runtime.NumCPU()-wide, making the network peak machine-dependent
MaxParallel: 2, MaxParallel: 2,
ForceRebuild: true, ForceRebuild: true,
Env: cfg.Env, Env: cfg.Env,
Secrets: cfg.Secrets, Secrets: cfg.Secrets,
Inputs: cfg.Inputs,
GitHubInstance: "github.com", GitHubInstance: "github.com",
DefaultActionInstance: cfg.DefaultActionInstance, DefaultActionInstance: cfg.DefaultActionInstance,
ContainerArchitecture: cfg.ContainerArchitecture, ContainerArchitecture: cfg.ContainerArchitecture,
ContainerMaxLifetime: time.Hour, ContainerMaxLifetime: time.Hour,
Matrix: cfg.Matrix,
ActionCache: cfg.ActionCache,
ValidVolumes: []string{"**"}, // allow workflow-declared volumes (e.g. container-volumes) ValidVolumes: []string{"**"}, // allow workflow-declared volumes (e.g. container-volumes)
} }
@@ -239,10 +241,6 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
fmt.Println("::endgroup::") //nolint:forbidigo // pre-existing issue from nektos/act fmt.Println("::endgroup::") //nolint:forbidigo // pre-existing issue from nektos/act
} }
type TestConfig struct {
LocalRepositories map[string]string `yaml:"local-repositories"`
}
func TestRunEvent(t *testing.T) { func TestRunEvent(t *testing.T) {
requireDocker(t) requireDocker(t)
t.Parallel() t.Parallel()
@@ -323,9 +321,6 @@ func TestRunEvent(t *testing.T) {
{workdir, "services", "push", "", platforms, secrets}, {workdir, "services", "push", "", platforms, secrets},
{workdir, "services-with-container", "push", "", platforms, secrets}, {workdir, "services-with-container", "push", "", platforms, secrets},
{workdir, "services-empty-image", "push", "", platforms, secrets}, {workdir, "services-empty-image", "push", "", platforms, secrets},
// local remote action overrides
{workdir, "local-remote-action-overrides", "push", "", platforms, secrets},
} }
for _, table := range tables { for _, table := range tables {
@@ -347,22 +342,6 @@ func TestRunEvent(t *testing.T) {
config.EventPath = eventFile config.EventPath = eventFile
} }
testConfigFile := filepath.Join(workdir, table.workflowPath, "config.yml")
if file, err := os.ReadFile(testConfigFile); err == nil {
testConfig := &TestConfig{}
if yaml.Unmarshal(file, testConfig) == nil {
if testConfig.LocalRepositories != nil {
config.ActionCache = &LocalRepositoryCache{
Parent: GoGitActionCache{
path.Clean(path.Join(workdir, "cache")),
},
LocalRepositories: testConfig.LocalRepositories,
CacheDirCache: map[string]string{},
}
}
}
}
table.runTest(ctx, t, config) table.runTest(ctx, t, config)
}) })
} }
@@ -584,8 +563,7 @@ func TestRunWithService(t *testing.T) {
runnerConfig := &Config{ runnerConfig := &Config{
Workdir: workdir, Workdir: workdir,
EventName: eventName, EventName: eventName,
Platforms: platforms, PlatformPicker: mapPlatformPicker(platforms),
ReuseContainers: false,
ContainerMaxLifetime: time.Hour, // otherwise the job container is `sleep 0` and exits at once ContainerMaxLifetime: time.Hour, // otherwise the job container is `sleep 0` and exits at once
} }
runner, err := New(runnerConfig) runner, err := New(runnerConfig)
@@ -601,26 +579,6 @@ func TestRunWithService(t *testing.T) {
assert.NoError(t, err, workflowPath) assert.NoError(t, err, workflowPath)
} }
func TestRunActionInputs(t *testing.T) {
t.Parallel()
requireDocker(t)
workflowPath := "input-from-cli"
tjfi := TestJobFileInfo{
workdir: workdir,
workflowPath: workflowPath,
eventName: "workflow_dispatch",
errorMessage: "",
platforms: platforms,
}
inputs := map[string]string{
"SOME_INPUT": "input",
}
tjfi.runTest(context.Background(), t, &Config{Inputs: inputs})
}
func TestRunEventPullRequest(t *testing.T) { func TestRunEventPullRequest(t *testing.T) {
t.Parallel() t.Parallel()
requireDocker(t) requireDocker(t)
@@ -637,29 +595,3 @@ func TestRunEventPullRequest(t *testing.T) {
tjfi.runTest(context.Background(), t, &Config{EventPath: filepath.Join(workdir, workflowPath, "event.json")}) tjfi.runTest(context.Background(), t, &Config{EventPath: filepath.Join(workdir, workflowPath, "event.json")})
} }
func TestRunMatrixWithUserDefinedInclusions(t *testing.T) {
t.Parallel()
requireDocker(t)
workflowPath := "matrix-with-user-inclusions"
tjfi := TestJobFileInfo{
workdir: workdir,
workflowPath: workflowPath,
eventName: "push",
errorMessage: "",
platforms: platforms,
}
matrix := map[string]map[string]bool{
"node": {
"8": true,
"8.x": true,
},
"os": {
"ubuntu-18.04": true,
},
}
tjfi.runTest(context.Background(), t, &Config{Matrix: matrix})
}
+3 -11
View File
@@ -85,10 +85,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
rc.StepResults[rc.CurrentStep] = stepResult rc.StepResults[rc.CurrentStep] = stepResult
} }
err := setupEnv(ctx, step) setupEnv(ctx, step)
if err != nil {
return err
}
runStep, err := isStepEnabled(ctx, ifExpression, step, stage) runStep, err := isStepEnabled(ctx, ifExpression, step, stage)
if err != nil { if err != nil {
@@ -232,7 +229,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
} }
} }
func evaluateStepTimeout(ctx context.Context, exprEval ExpressionEvaluator, stepModel *model.Step) (context.Context, context.CancelFunc) { func evaluateStepTimeout(ctx context.Context, exprEval *expressionEvaluator, stepModel *model.Step) (context.Context, context.CancelFunc) {
timeout := exprEval.Interpolate(ctx, stepModel.TimeoutMinutes) timeout := exprEval.Interpolate(ctx, stepModel.TimeoutMinutes)
if timeout != "" { if timeout != "" {
if timeOutMinutes, err := strconv.ParseInt(timeout, 10, 64); err == nil { if timeOutMinutes, err := strconv.ParseInt(timeout, 10, 64); err == nil {
@@ -242,7 +239,7 @@ func evaluateStepTimeout(ctx context.Context, exprEval ExpressionEvaluator, step
return ctx, func() {} return ctx, func() {}
} }
func setupEnv(ctx context.Context, step step) error { //nolint:unparam // pre-existing issue from nektos/act func setupEnv(ctx context.Context, step step) {
rc := step.getRunContext() rc := step.getRunContext()
mergeEnv(ctx, step) mergeEnv(ctx, step)
@@ -263,11 +260,6 @@ func setupEnv(ctx context.Context, step step) error { //nolint:unparam // pre-ex
(*step.getEnv())[k] = exprEval.Interpolate(ctx, v) (*step.getEnv())[k] = exprEval.Interpolate(ctx, v)
} }
} }
// For Gitea, reduce log noise
// common.Logger(ctx).Debugf("setupEnv => %v", *step.getEnv())
return nil
} }
func mergeEnv(ctx context.Context, step step) { func mergeEnv(ctx context.Context, step step) {
+6 -11
View File
@@ -33,10 +33,7 @@ type stepActionLocal struct {
func (sal *stepActionLocal) pre() common.Executor { func (sal *stepActionLocal) pre() common.Executor {
sal.env = map[string]string{} sal.env = map[string]string{}
return common.NewPipelineExecutor()
return func(ctx context.Context) error {
return nil
}
} }
func (sal *stepActionLocal) main() common.Executor { func (sal *stepActionLocal) main() common.Executor {
@@ -50,11 +47,10 @@ func (sal *stepActionLocal) main() common.Executor {
defer rawLogger.Infof("::endgroup::") defer rawLogger.Infof("::endgroup::")
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses) actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)
_, containerActionPath := getContainerActionPaths(sal.Step, path.Join(actionDir, ""), sal.RunContext)
localReader := func(ctx context.Context) actionYamlReader { localReader := func(filename string) (io.Reader, io.Closer, error) {
_, cpath := getContainerActionPaths(sal.Step, path.Join(actionDir, ""), sal.RunContext) spath := path.Join(containerActionPath, filename)
return func(filename string) (io.Reader, io.Closer, error) {
spath := path.Join(cpath, filename)
for range maxSymlinkDepth { for range maxSymlinkDepth {
tars, err := sal.RunContext.JobContainer.GetContainerArchive(ctx, spath) tars, err := sal.RunContext.JobContainer.GetContainerArchive(ctx, spath)
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
@@ -70,7 +66,7 @@ func (sal *stepActionLocal) main() common.Executor {
return nil, nil, err return nil, nil, err
} }
if header.FileInfo().Mode()&os.ModeSymlink == os.ModeSymlink { if header.FileInfo().Mode()&os.ModeSymlink == os.ModeSymlink {
spath, err = symlinkJoin(spath, header.Linkname, cpath) spath, err = symlinkJoin(spath, header.Linkname, containerActionPath)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -80,9 +76,8 @@ func (sal *stepActionLocal) main() common.Executor {
} }
return nil, nil, fmt.Errorf("max depth %d of symlinks exceeded while reading %s", maxSymlinkDepth, spath) return nil, nil, fmt.Errorf("max depth %d of symlinks exceeded while reading %s", maxSymlinkDepth, spath)
} }
}
actionModel, err := sal.readAction(ctx, sal.Step, actionDir, "", localReader(ctx), os.WriteFile) actionModel, err := sal.readAction(ctx, sal.Step, actionDir, "", localReader, os.WriteFile)
if err != nil { if err != nil {
return err return err
} }
+9 -27
View File
@@ -74,27 +74,17 @@ func TestStepActionLocalTest(t *testing.T) {
salm.On("readAction", sal.Step, filepath.Clean("/tmp/path/to/action"), "", mock.Anything, mock.Anything). salm.On("readAction", sal.Step, filepath.Clean("/tmp/path/to/action"), "", mock.Anything, mock.Anything).
Return(&model.Action{}, nil) Return(&model.Action{}, nil)
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error { cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil) cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
salm.On("runAction", sal, filepath.Clean("/tmp/path/to/action"), (*remoteAction)(nil)).Return(func(ctx context.Context) error { salm.On("runAction", sal, filepath.Clean("/tmp/path/to/action"), (*remoteAction)(nil)).Return(noopExecutor)
return nil
})
err := sal.pre()(ctx) err := sal.pre()(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
@@ -269,21 +259,13 @@ func TestStepActionLocalPost(t *testing.T) {
} }
cm.On("Exec", suffixMatcher("runner/local/action/post.js"), sal.env, "", "").Return(func(ctx context.Context) error { return tt.err }) cm.On("Exec", suffixMatcher("runner/local/action/post.js"), sal.env, "", "").Return(func(ctx context.Context) error { return tt.err })
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error { cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil) cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
} }
+2 -58
View File
@@ -5,7 +5,6 @@
package runner package runner
import ( import (
"archive/tar"
"context" "context"
"errors" "errors"
"fmt" "fmt"
@@ -33,7 +32,6 @@ type stepActionRemote struct {
action *model.Action action *model.Action
env map[string]string env map[string]string
remoteAction *remoteAction remoteAction *remoteAction
cacheDir string
resolvedSha string resolvedSha string
} }
@@ -69,58 +67,6 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
common.Logger(ctx).Debugf("Skipping local actions/checkout because workdir was already copied") common.Logger(ctx).Debugf("Skipping local actions/checkout because workdir was already copied")
return nil return nil
} }
for _, action := range sar.RunContext.Config.ReplaceGheActionWithGithubCom {
if strings.EqualFold(fmt.Sprintf("%s/%s", sar.remoteAction.Org, sar.remoteAction.Repo), action) {
sar.remoteAction.URL = "https://github.com"
github.Token = sar.RunContext.Config.ReplaceGheActionTokenWithGithubCom
}
}
// Actions served from the action cache are read out of a git object store rather than a
// directory, so they never reach the bundle patch below and keep to the v1 cache API.
if sar.RunContext.Config.ActionCache != nil {
cache := sar.RunContext.Config.ActionCache
var err error
sar.cacheDir = fmt.Sprintf("%s/%s", sar.remoteAction.Org, sar.remoteAction.Repo)
repoURL := sar.remoteAction.URL + "/" + sar.cacheDir
repoRef := sar.remoteAction.Ref
sar.resolvedSha, err = cache.Fetch(ctx, sar.cacheDir, repoURL, repoRef, github.Token)
if err != nil {
return fmt.Errorf("failed to fetch \"%s\" version \"%s\": %w", repoURL, repoRef, err)
}
remoteReader := func(ctx context.Context) actionYamlReader {
return func(filename string) (io.Reader, io.Closer, error) {
spath := path.Join(sar.remoteAction.Path, filename)
for range maxSymlinkDepth {
tars, err := cache.GetTarArchive(ctx, sar.cacheDir, sar.resolvedSha, spath)
if err != nil {
return nil, nil, os.ErrNotExist
}
treader := tar.NewReader(tars)
header, err := treader.Next()
if err != nil {
return nil, nil, os.ErrNotExist
}
if header.FileInfo().Mode()&os.ModeSymlink == os.ModeSymlink {
spath, err = symlinkJoin(spath, header.Linkname, ".")
if err != nil {
return nil, nil, err
}
} else {
return treader, tars, nil
}
}
return nil, nil, fmt.Errorf("max depth %d of symlinks exceeded while reading %s", maxSymlinkDepth, spath)
}
}
actionModel, err := sar.readAction(ctx, sar.Step, sar.resolvedSha, sar.remoteAction.Path, remoteReader(ctx), os.WriteFile)
sar.action = actionModel
return err
}
actionDir := sar.actionDir() actionDir := sar.actionDir()
defaultActionURL := sar.RunContext.Config.DefaultActionURL() defaultActionURL := sar.RunContext.Config.DefaultActionURL()
// For Gitea // For Gitea
@@ -165,18 +111,16 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
sar.resolvedSha = sha sar.resolvedSha = sha
} }
remoteReader := func(ctx context.Context) actionYamlReader { //nolint:unparam // pre-existing issue from nektos/act remoteReader := func(filename string) (io.Reader, io.Closer, error) {
return func(filename string) (io.Reader, io.Closer, error) {
f, err := os.Open(filepath.Join(actionDir, sar.remoteAction.Path, filename)) f, err := os.Open(filepath.Join(actionDir, sar.remoteAction.Path, filename))
return f, f, err return f, f, err
} }
}
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
ntErr, ntErr,
func(ctx context.Context) error { func(ctx context.Context) error {
defer git.AcquireCloneLock(actionDir)() 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, os.WriteFile)
sar.action = actionModel sar.action = actionModel
return err return err
}, },
+83 -308
View File
@@ -31,6 +31,52 @@ type stepActionRemoteMocks struct {
mock.Mock mock.Mock
} }
func actionDirSuffix(suffix string) any {
return mock.MatchedBy(func(actionDir string) bool { return strings.HasSuffix(actionDir, suffix) })
}
func setCloneExecutor(t *testing.T, executor func(git.NewGitCloneExecutorInput) common.Executor) {
original := stepActionRemoteNewCloneExecutor
stepActionRemoteNewCloneExecutor = executor
t.Cleanup(func() { stepActionRemoteNewCloneExecutor = original })
}
func TestShortSHAActionRejected(t *testing.T) {
actionRoot := t.TempDir()
repo := filepath.Join(actionRoot, "actions", "hello-world-docker-action")
require.NoError(t, os.MkdirAll(repo, 0o755))
gitMust(t, "", "init", "--initial-branch=main", repo)
gitMust(t, repo, "config", "user.email", "test@test")
gitMust(t, repo, "config", "user.name", "test")
require.NoError(t, os.WriteFile(filepath.Join(repo, "action.yml"),
[]byte("name: hello\nruns:\n using: node24\n main: index.js\n"), 0o644))
gitMust(t, repo, "add", ".")
gitMust(t, repo, "commit", "-m", "initial")
output, err := exec.Command("git", "-C", repo, "rev-parse", "--short=7", "HEAD").Output()
require.NoError(t, err)
workflowDir := t.TempDir()
workflow := fmt.Sprintf("on: push\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/hello-world-docker-action@%s\n", strings.TrimSpace(string(output)))
require.NoError(t, os.WriteFile(filepath.Join(workflowDir, "push.yml"), []byte(workflow), 0o644))
runner, err := New(&Config{
Workdir: workflowDir,
EventName: "push",
GitHubInstance: "github.com",
DefaultActionInstance: actionRoot,
ContainerMaxLifetime: time.Hour,
PlatformPicker: func([]string) string { return baseImage },
})
require.NoError(t, err)
planner, err := model.NewWorkflowPlanner(workflowDir, true)
require.NoError(t, err)
plan, err := planner.PlanEvent("push")
require.NoError(t, err)
err = runner.NewPlanExecutor(plan)(common.WithDryrun(t.Context(), true))
require.ErrorContains(t, err, "shortened version of a commit SHA")
}
func (sarm *stepActionRemoteMocks) readAction(_ context.Context, step *model.Step, actionDir, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) { func (sarm *stepActionRemoteMocks) readAction(_ context.Context, step *model.Step, actionDir, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
args := sarm.Called(step, actionDir, actionPath, readFile, writeFile) args := sarm.Called(step, actionDir, actionPath, readFile, writeFile)
return args.Get(0).(*model.Action), args.Error(1) return args.Get(0).(*model.Action), args.Error(1)
@@ -136,16 +182,12 @@ func TestStepActionRemote(t *testing.T) {
clonedAction := false clonedAction := false
origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor setCloneExecutor(t, func(input git.NewGitCloneExecutorInput) common.Executor {
stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor {
return func(ctx context.Context) error { return func(ctx context.Context) error {
clonedAction = true clonedAction = true
return nil return nil
} }
} })
defer (func() {
stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
})()
sar := &stepActionRemote{ sar := &stepActionRemote{
RunContext: &RunContext{ RunContext: &RunContext{
@@ -170,33 +212,19 @@ func TestStepActionRemote(t *testing.T) {
} }
sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx) sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx)
suffixMatcher := func(suffix string) any {
return mock.MatchedBy(func(actionDir string) bool {
return strings.HasSuffix(actionDir, suffix)
})
}
if tt.mocks.read { if tt.mocks.read {
sarm.On("readAction", sar.Step, suffixMatcher(sar.Step.UsesHash()), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil) sarm.On("readAction", sar.Step, actionDirSuffix(sar.Step.UsesHash()), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
} }
if tt.mocks.run { if tt.mocks.run {
sarm.On("runAction", sar, suffixMatcher(sar.Step.UsesHash()), newRemoteAction(sar.Step.Uses)).Return(func(ctx context.Context) error { return tt.runError }) sarm.On("runAction", sar, actionDirSuffix(sar.Step.UsesHash()), newRemoteAction(sar.Step.Uses)).Return(func(ctx context.Context) error { return tt.runError })
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error { cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil) cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
} }
@@ -216,277 +244,41 @@ func TestStepActionRemote(t *testing.T) {
} }
} }
func TestStepActionRemotePre(t *testing.T) { func TestStepActionRemotePrepare(t *testing.T) {
table := []struct { for _, test := range []struct {
name string name, uses, instance, actionPath, wantURL string
stepModel *model.Step
}{ }{
{ {name: "nested action", uses: "org/repo/path@ref", instance: "https://github.com", actionPath: "path", wantURL: "https://github.com/org/repo"},
name: "run-pre", {name: "instance fallback", uses: "actions/setup-go@v4", instance: "gitea.example", wantURL: "https://gitea.example/actions/setup-go"},
stepModel: &model.Step{
Uses: "org/repo/path@ref",
},
},
}
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
clonedAction := false
sarm := &stepActionRemoteMocks{}
origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor
stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor {
return func(ctx context.Context) error {
clonedAction = true
return nil
}
}
defer (func() {
stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
})()
sar := &stepActionRemote{
Step: tt.stepModel,
RunContext: &RunContext{
Config: &Config{
GitHubInstance: "https://github.com",
ActionCacheDir: "/tmp/test-cache",
},
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()), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
err := sar.pre()(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.True(t, clonedAction)
sarm.AssertExpectations(t)
})
}
}
func TestStepActionRemotePreThroughAction(t *testing.T) {
table := []struct {
name string
stepModel *model.Step
} { } {
{ t.Run(test.name, func(t *testing.T) {
name: "run-pre",
stepModel: &model.Step{
Uses: "org/repo/path@ref",
},
},
}
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
clonedAction := false
sarm := &stepActionRemoteMocks{}
origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor
stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor {
return func(ctx context.Context) error {
if input.URL == "https://github.com/org/repo" {
clonedAction = true
}
return nil
}
}
defer (func() {
stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
})()
sar := &stepActionRemote{
Step: tt.stepModel,
RunContext: &RunContext{
Config: &Config{
GitHubInstance: "https://enterprise.github.com",
ReplaceGheActionWithGithubCom: []string{"org/repo"},
ActionCacheDir: "/tmp/test-cache",
},
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()), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
err := sar.pre()(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
assert.True(t, clonedAction)
sarm.AssertExpectations(t)
})
}
}
func TestStepActionRemotePreThroughActionToken(t *testing.T) {
table := []struct {
name string
stepModel *model.Step
}{
{
name: "run-pre",
stepModel: &model.Step{
Uses: "org/repo/path@ref",
},
},
}
for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
var actualURL string var actualURL string
var actualToken string setCloneExecutor(t, func(input git.NewGitCloneExecutorInput) common.Executor {
sarm := &stepActionRemoteMocks{} return func(context.Context) error {
origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor
stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor {
return func(ctx context.Context) error {
actualURL = input.URL
actualToken = input.Token
return nil
}
}
defer (func() {
stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
})()
// Use unique cache directory to ensure action gets cloned, not served from cache
uniqueCacheDir := fmt.Sprintf("/tmp/test-cache-token-%d", time.Now().UnixNano())
sar := &stepActionRemote{
Step: tt.stepModel,
RunContext: &RunContext{
Config: &Config{
GitHubInstance: "https://enterprise.github.com",
ReplaceGheActionWithGithubCom: []string{"org/repo"},
ReplaceGheActionTokenWithGithubCom: "PRIVATE_ACTIONS_TOKEN_ON_GITHUB",
ActionCacheDir: uniqueCacheDir,
Token: "PRIVATE_ACTIONS_TOKEN_ON_GITHUB",
},
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()), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
err := sar.pre()(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
// Verify that the clone was called (URL should be redirected to github.com)
assert.True(t, actualURL != "", "Expected clone to be called") //nolint:testifylint // pre-existing issue from nektos/act
assert.Equal(t, "https://github.com/org/repo", actualURL, "URL should be redirected to github.com")
// Note: Token might be empty because getGitCloneToken doesn't check ReplaceGheActionTokenWithGithubCom
// The important part is that the URL replacement works
if actualToken != "" {
assert.Equal(t, "PRIVATE_ACTIONS_TOKEN_ON_GITHUB", actualToken, "If token is set, it should be the replacement token")
}
sarm.AssertExpectations(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 actualURL = input.URL
return nil return nil
} }
} })
defer func() {
stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
}()
sar := &stepActionRemote{ actionMocks := &stepActionRemoteMocks{}
Step: &model.Step{ action := &stepActionRemote{
Uses: "actions/setup-go@v4", Step: &model.Step{Uses: test.uses},
},
RunContext: &RunContext{ RunContext: &RunContext{
Config: &Config{ Config: &Config{GitHubInstance: test.instance, ActionCacheDir: t.TempDir()},
GitHubInstance: "gitea.example", Run: &model.Run{JobID: "1", Workflow: &model.Workflow{
DefaultActionInstance: "", Jobs: map[string]*model.Job{"1": {}},
ActionCacheDir: t.TempDir(), }},
}, },
Run: &model.Run{ readAction: actionMocks.readAction,
JobID: "1",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"1": {},
},
},
},
},
readAction: sarm.readAction,
} }
actionMocks.On("readAction", action.Step, actionDirSuffix(action.Step.UsesHash()), test.actionPath,
mock.Anything, mock.Anything).Return(&model.Action{}, nil)
suffixMatcher := func(suffix string) any { require.NoError(t, action.prepareActionExecutor()(t.Context()))
return mock.MatchedBy(func(actionDir string) bool { assert.Equal(t, test.wantURL, actualURL)
return strings.HasSuffix(actionDir, suffix) actionMocks.AssertExpectations(t)
}) })
} }
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) { func TestStepActionRemotePost(t *testing.T) {
@@ -669,21 +461,13 @@ func TestStepActionRemotePost(t *testing.T) {
cm.On("Exec", execMatcher, sar.env, "", "").Return(func(ctx context.Context) error { return tt.err }) cm.On("Exec", execMatcher, sar.env, "", "").Return(func(ctx context.Context) error { return tt.err })
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error { cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil) cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
} }
@@ -1032,14 +816,10 @@ func TestStepActionRemoteCloneTokenSurvivesNilSecrets(t *testing.T) {
ctx := context.Background() ctx := context.Background()
var capturedToken string var capturedToken string
origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor setCloneExecutor(t, func(input git.NewGitCloneExecutorInput) common.Executor {
stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor {
capturedToken = input.Token capturedToken = input.Token
return func(ctx context.Context) error { return nil } return func(ctx context.Context) error { return nil }
} })
defer (func() {
stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
})()
sarm := &stepActionRemoteMocks{} sarm := &stepActionRemoteMocks{}
sar := &stepActionRemote{ sar := &stepActionRemote{
@@ -1066,12 +846,7 @@ func TestStepActionRemoteCloneTokenSurvivesNilSecrets(t *testing.T) {
} }
sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx) sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx)
suffixMatcher := func(suffix string) any { sarm.On("readAction", sar.Step, actionDirSuffix(sar.Step.UsesHash()), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
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)
err := sar.prepareActionExecutor()(ctx) err := sar.prepareActionExecutor()(ctx)
require.NoError(t, err) require.NoError(t, err)
+4 -62
View File
@@ -6,7 +6,6 @@ package runner
import ( import (
"context" "context"
"fmt"
"strings" "strings"
"gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/common"
@@ -22,11 +21,7 @@ type stepDocker struct {
env map[string]string env map[string]string
} }
func (sd *stepDocker) pre() common.Executor { func (sd *stepDocker) pre() common.Executor { return common.NewPipelineExecutor() }
return func(ctx context.Context) error {
return nil
}
}
func (sd *stepDocker) main() common.Executor { func (sd *stepDocker) main() common.Executor {
sd.env = map[string]string{} sd.env = map[string]string{}
@@ -34,11 +29,7 @@ func (sd *stepDocker) main() common.Executor {
return runStepExecutor(sd, stepStageMain, sd.runUsesContainer()) return runStepExecutor(sd, stepStageMain, sd.runUsesContainer())
} }
func (sd *stepDocker) post() common.Executor { func (sd *stepDocker) post() common.Executor { return common.NewPipelineExecutor() }
return func(ctx context.Context) error {
return nil
}
}
func (sd *stepDocker) getRunContext() *RunContext { func (sd *stepDocker) getRunContext() *RunContext {
return sd.RunContext return sd.RunContext
@@ -77,64 +68,15 @@ func (sd *stepDocker) runUsesContainer() common.Executor {
entrypoint = []string{entry} entrypoint = []string{entry}
} }
stepContainer := sd.newStepContainer(ctx, image, cmd, entrypoint) stepContainer := newStepContainer(ctx, sd, image, cmd, entrypoint, "")
return common.NewPipelineExecutor( return common.NewPipelineExecutor(
stepContainer.Pull(rc.Config.ForcePull), stepContainer.Pull(rc.Config.ForcePull),
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers), stepContainer.Remove(),
stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop), stepContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
stepContainer.Start(true), stepContainer.Start(true),
).Finally(
stepContainer.Remove().IfBool(!rc.Config.ReuseContainers && !rc.Config.AutoRemove),
).Finally(stepContainer.Close())(ctx) ).Finally(stepContainer.Close())(ctx)
} }
} }
var ContainerNewContainer = container.NewContainer var ContainerNewContainer = container.NewContainer
func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd, entrypoint []string) container.Container {
rc := sd.RunContext
step := sd.Step
rawLogger := common.Logger(ctx).WithField("raw_output", true)
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
if rc.Config.LogOutput {
rawLogger.Infof("%s", s)
} else {
rawLogger.Debugf("%s", s)
}
return true
})
envList := make([]string, 0)
for k, v := range sd.env {
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
}
envList = append(envList, rc.runnerEnv(ctx)...)
binds, mounts := rc.GetBindsAndMounts()
networkMode := "container:" + rc.jobContainerName()
if rc.IsHostEnv(ctx) {
networkMode = "default"
}
stepContainer := ContainerNewContainer(&container.NewContainerInput{
Cmd: cmd,
Entrypoint: entrypoint,
WorkingDir: rc.JobContainer.ToContainerPath(rc.Config.Workdir),
Image: image,
Name: createContainerName(rc.jobContainerName(), "STEP-"+step.ID),
Env: envList,
Mounts: mounts,
NetworkMode: networkMode,
Binds: binds,
Stdout: logWriter,
Stderr: logWriter,
Privileged: rc.Config.Privileged,
UsernsMode: rc.Config.UsernsMode,
Platform: rc.Config.ContainerArchitecture,
AutoRemove: rc.Config.AutoRemove,
ValidVolumes: rc.validVolumes(),
AllocatePTY: rc.Config.AllocatePTY,
})
return stepContainer
}
+12 -78
View File
@@ -16,7 +16,6 @@ import (
"gitea.dev/actionslib/pkg/model" "gitea.dev/actionslib/pkg/model"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
) )
func TestStepDockerMain(t *testing.T) { func TestStepDockerMain(t *testing.T) {
@@ -69,41 +68,23 @@ func TestStepDockerMain(t *testing.T) {
} }
sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx) sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx)
cm.On("Pull", false).Return(func(ctx context.Context) error { cm.On("Pull", false).Return(noopExecutor)
return nil
})
cm.On("Remove").Return(func(ctx context.Context) error { cm.On("Remove").Return(noopExecutor)
return nil
})
cm.On("Create", []string(nil), []string(nil)).Return(func(ctx context.Context) error { cm.On("Create", []string(nil), []string(nil)).Return(noopExecutor)
return nil
})
cm.On("Start", true).Return(func(ctx context.Context) error { cm.On("Start", true).Return(noopExecutor)
return nil
})
cm.On("Close").Return(func(ctx context.Context) error { cm.On("Close").Return(noopExecutor)
return nil
})
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error { cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil) cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
@@ -115,47 +96,11 @@ func TestStepDockerMain(t *testing.T) {
// DOCKER_USERNAME/DOCKER_PASSWORD secrets should not be used as implicit pull credentials for docker:// action containers. // DOCKER_USERNAME/DOCKER_PASSWORD secrets should not be used as implicit pull credentials for docker:// action containers.
assert.Empty(t, input.Username) assert.Empty(t, input.Username)
assert.Empty(t, input.Password) assert.Empty(t, input.Password)
assert.True(t, input.AutoRemove)
cm.AssertExpectations(t) cm.AssertExpectations(t)
} }
// With AutoRemove the daemon reaps the container on exit, so act must not remove it afterwards.
func TestStepDockerAutoRemove(t *testing.T) {
orig := ContainerNewContainer
defer func() { ContainerNewContainer = orig }()
for _, tc := range []struct {
autoRemove bool
removes int
}{
{false, 2}, // stale + post-run
{true, 1}, // post-run skipped
} {
cm := &containerMock{}
ContainerNewContainer = func(*container.NewContainerInput) container.ExecutionsEnvironment { return cm }
sd := &stepDocker{
RunContext: &RunContext{
Config: &Config{AutoRemove: tc.autoRemove},
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"},
}
removes := 0
cm.On("Pull", false).Return(func(context.Context) error { return nil })
cm.On("Remove").Return(func(context.Context) error { removes++; return nil })
cm.On("Create", []string(nil), []string(nil)).Return(func(context.Context) error { return nil })
cm.On("Start", true).Return(func(context.Context) error { return nil })
cm.On("Close").Return(func(context.Context) error { return nil })
require.NoError(t, sd.runUsesContainer()(context.Background()))
cm.AssertExpectations(t)
assert.Equal(t, tc.removes, removes)
}
}
func TestStepDockerNewStepContainerAllocatePTY(t *testing.T) { func TestStepDockerNewStepContainerAllocatePTY(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
name string name string
@@ -199,23 +144,12 @@ func TestStepDockerNewStepContainerAllocatePTY(t *testing.T) {
} }
sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx) sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx)
_ = sd.newStepContainer(ctx, "node:14", []string{"echo", "hi"}, nil) _ = newStepContainer(ctx, sd, "node:14", []string{"echo", "hi"}, nil, "")
assert.Equal(t, tc.allocPTY, captured.AllocatePTY) assert.Equal(t, tc.allocPTY, captured.AllocatePTY)
}) })
} }
} }
func TestStepDockerPrePost(t *testing.T) {
ctx := context.Background()
sd := &stepDocker{}
err := sd.pre()(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
err = sd.post()(ctx)
assert.NoError(t, err)
}
func TestStepDockerNewStepContainerNetworkMode(t *testing.T) { func TestStepDockerNewStepContainerNetworkMode(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
@@ -279,7 +213,7 @@ func TestStepDockerNewStepContainerNetworkMode(t *testing.T) {
assert.Equal(t, tc.expectDefault, sd.RunContext.IsHostEnv(ctx), assert.Equal(t, tc.expectDefault, sd.RunContext.IsHostEnv(ctx),
"IsHostEnv mismatch for platform %q", tc.platform) "IsHostEnv mismatch for platform %q", tc.platform)
_ = sd.newStepContainer(ctx, "alpine:3.20", []string{"echo", "hello"}, nil) _ = newStepContainer(ctx, sd, "alpine:3.20", []string{"echo", "hello"}, nil, "")
if tc.expectDefault { if tc.expectDefault {
assert.Equal(t, "default", captured.NetworkMode, assert.Equal(t, "default", captured.NetworkMode,
+5 -30
View File
@@ -33,11 +33,7 @@ type stepRun struct {
shellCommand string shellCommand string
} }
func (sr *stepRun) pre() common.Executor { func (sr *stepRun) pre() common.Executor { return common.NewPipelineExecutor() }
return func(ctx context.Context) error {
return nil
}
}
func (sr *stepRun) main() common.Executor { func (sr *stepRun) main() common.Executor {
sr.env = map[string]string{} sr.env = map[string]string{}
@@ -202,11 +198,7 @@ func stepDeclaredEnvKeysInOrder(step *model.Step) []string {
return keys return keys
} }
func (sr *stepRun) post() common.Executor { func (sr *stepRun) post() common.Executor { return common.NewPipelineExecutor() }
return func(ctx context.Context) error {
return nil
}
}
func (sr *stepRun) getRunContext() *RunContext { func (sr *stepRun) getRunContext() *RunContext {
return sr.RunContext return sr.RunContext
@@ -306,22 +298,6 @@ func (sr *stepRun) setupShellCommand(ctx context.Context) (name, script string,
return name, script, err return name, script, err
} }
type localEnv struct {
env map[string]string
}
func (l *localEnv) Getenv(name string) string {
if runtime.GOOS == "windows" {
for k, v := range l.env {
if strings.EqualFold(name, k) {
return v
}
}
return ""
}
return l.env[name]
}
func (sr *stepRun) setupShell(ctx context.Context) { func (sr *stepRun) setupShell(ctx context.Context) {
rc := sr.RunContext rc := sr.RunContext
step := sr.Step step := sr.Step
@@ -344,10 +320,9 @@ func (sr *stepRun) setupShell(ctx context.Context) {
shellWithFallback = []string{"pwsh", "powershell"} shellWithFallback = []string{"pwsh", "powershell"}
} }
step.Shell = shellWithFallback[0] step.Shell = shellWithFallback[0]
lenv := &localEnv{env: map[string]string{}} env := maps.Clone(sr.env)
maps.Copy(lenv.env, sr.env) sr.getRunContext().ApplyExtraPath(ctx, &env)
sr.getRunContext().ApplyExtraPath(ctx, &lenv.env) _, err := lookpath.LookPath2(shellWithFallback[0], env)
_, err := lookpath.LookPath2(shellWithFallback[0], lenv)
if err != nil { if err != nil {
step.Shell = shellWithFallback[1] step.Shell = shellWithFallback[1]
} }
+6 -29
View File
@@ -53,28 +53,16 @@ func TestStepRun(t *testing.T) {
}, },
} }
cm.On("Copy", "/var/run/act", []*container.FileEntry{fileEntry}).Return(func(ctx context.Context) error { cm.On("Copy", "/var/run/act", []*container.FileEntry{fileEntry}).Return(noopExecutor)
return nil cm.On("Exec", []string{"bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "/var/run/act/workflow/1.sh"}, mock.AnythingOfType("map[string]string"), "", "workdir").Return(noopExecutor)
})
cm.On("Exec", []string{"bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "/var/run/act/workflow/1.sh"}, mock.AnythingOfType("map[string]string"), "", "workdir").Return(func(ctx context.Context) error {
return nil
})
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error { cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
return nil
})
ctx := context.Background() ctx := context.Background()
@@ -85,14 +73,3 @@ func TestStepRun(t *testing.T) {
cm.AssertExpectations(t) cm.AssertExpectations(t)
} }
func TestStepRunPrePost(t *testing.T) {
ctx := context.Background()
sr := &stepRun{}
err := sr.pre()(ctx)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
err = sr.post()(ctx)
assert.NoError(t, err)
}
+3 -14
View File
@@ -157,8 +157,7 @@ func TestSetupEnv(t *testing.T) {
sm.On("getStepModel").Return(step) sm.On("getStepModel").Return(step)
sm.On("getEnv").Return(&env) sm.On("getEnv").Return(&env)
err := setupEnv(context.Background(), sm) setupEnv(context.Background(), sm)
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
// These are commit or system specific // These are commit or system specific
delete((env), "GITHUB_REF") delete((env), "GITHUB_REF")
@@ -213,12 +212,7 @@ func TestIsStepEnabled(t *testing.T) {
return &stepRun{ return &stepRun{
RunContext: &RunContext{ RunContext: &RunContext{
Config: &Config{ Config: &Config{Workdir: ".", PlatformPicker: func([]string) string { return "ubuntu-latest" }},
Workdir: ".",
Platforms: map[string]string{
"ubuntu-latest": "ubuntu-latest",
},
},
StepResults: map[string]*model.StepResult{}, StepResults: map[string]*model.StepResult{},
Env: map[string]string{}, Env: map[string]string{},
Run: &model.Run{ Run: &model.Run{
@@ -295,12 +289,7 @@ func TestIsContinueOnError(t *testing.T) {
return &stepRun{ return &stepRun{
RunContext: &RunContext{ RunContext: &RunContext{
Config: &Config{ Config: &Config{Workdir: ".", PlatformPicker: func([]string) string { return "ubuntu-latest" }},
Workdir: ".",
Platforms: map[string]string{
"ubuntu-latest": "ubuntu-latest",
},
},
StepResults: map[string]*model.StepResult{}, StepResults: map[string]*model.StepResult{},
Env: map[string]string{}, Env: map[string]string{},
Run: &model.Run{ Run: &model.Run{
@@ -1 +0,0 @@
ref: refs/heads/master
@@ -1,2 +0,0 @@
[core]
bare = true
-21
View File
@@ -1,21 +0,0 @@
on:
workflow_dispatch:
inputs:
NAME:
description: "A random input name for the workflow"
type: string
required: true
SOME_VALUE:
description: "Some other input to pass"
type: string
required: true
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Test with inputs
run: |
[ -z "${{ github.event.inputs.SOME_INPUT }}" ] && exit 1 || exit 0
@@ -1,3 +0,0 @@
local-repositories:
https://github.com/nektos/test-override@a: testdata/actions/node24
nektos/test-override@b: testdata/actions/node24
@@ -1,9 +0,0 @@
name: basic
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/nektos/test-override@a
- uses: nektos/test-override@b
@@ -1,34 +0,0 @@
name: matrix-with-user-inclusions
on: push
jobs:
build:
name: PHP ${{ matrix.os }} ${{ matrix.node}}
runs-on: ubuntu-latest
steps:
- run: |
echo ${NODE_VERSION} | grep 8
echo ${OS_VERSION} | grep ubuntu-18.04
env:
NODE_VERSION: ${{ matrix.node }}
OS_VERSION: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-18.04, macos-latest]
node: [4, 6, 8, 10]
exclude:
- os: macos-latest
node: 4
include:
- os: ubuntu-16.04
node: 10
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [8.x, 10.x, 12.x, 13.x]
steps:
- run: echo ${NODE_VERSION} | grep 8.x
env:
NODE_VERSION: ${{ matrix.node }}
-3
View File
@@ -446,10 +446,8 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
config := &runner.Config{ config := &runner.Config{
Workdir: execArgs.Workdir(), Workdir: execArgs.Workdir(),
BindWorkdir: false, BindWorkdir: false,
ReuseContainers: false,
ForcePull: execArgs.forcePull, ForcePull: execArgs.forcePull,
ForceRebuild: execArgs.forceRebuild, ForceRebuild: execArgs.forceRebuild,
LogOutput: true,
JSONLogger: execArgs.jsonLogger, JSONLogger: execArgs.jsonLogger,
Env: env, Env: env,
ProxyEnv: proxyEnv, ProxyEnv: proxyEnv,
@@ -465,7 +463,6 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
ContainerCapAdd: execArgs.containerCapAdd, ContainerCapAdd: execArgs.containerCapAdd,
ContainerCapDrop: execArgs.containerCapDrop, ContainerCapDrop: execArgs.containerCapDrop,
ContainerOptions: execArgs.containerOptions, ContainerOptions: execArgs.containerOptions,
AutoRemove: true,
ArtifactServerPath: execArgs.artifactServerPath, ArtifactServerPath: execArgs.artifactServerPath,
ArtifactServerPort: execArgs.artifactServerPort, ArtifactServerPort: execArgs.artifactServerPort,
ArtifactServerAddr: execArgs.artifactServerAddr, ArtifactServerAddr: execArgs.artifactServerAddr,
-10
View File
@@ -202,13 +202,6 @@ func (r *Runner) shouldRunIdleCleanup() bool {
} }
} }
// cleanupStaleTaskDirs reclaims stale bind-workdir per-task directories under
// workdirRoot. Retained as a thin wrapper so existing callers and tests keep a
// stable entry point.
func (r *Runner) cleanupStaleTaskDirs(ctx context.Context, workdirRoot string) {
r.cleanupStaleDirs(ctx, workdirRoot, isTaskIDDir)
}
// isTaskIDDir reports whether name is a per-task workspace dir (numeric task // isTaskIDDir reports whether name is a per-task workspace dir (numeric task
// ID). Any other directory is skipped to avoid deleting operator-managed data // ID). Any other directory is skipped to avoid deleting operator-managed data
// under workdir_root. // under workdir_root.
@@ -520,16 +513,13 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
ActionCloneDepth: actionCloneDepth, ActionCloneDepth: actionCloneDepth,
NoActionPatch: r.cfg.Runner.PatchActions != nil && !*r.cfg.Runner.PatchActions, NoActionPatch: r.cfg.Runner.PatchActions != nil && !*r.cfg.Runner.PatchActions,
ReuseContainers: false,
ForcePull: r.cfg.Container.ForcePull, ForcePull: r.cfg.Container.ForcePull,
ForceRebuild: r.cfg.Container.ForceRebuild, ForceRebuild: r.cfg.Container.ForceRebuild,
LogOutput: true,
JSONLogger: false, JSONLogger: false,
Env: envs, Env: envs,
ProxyEnv: proxyEnv, ProxyEnv: proxyEnv,
Secrets: task.Secrets, Secrets: task.Secrets,
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"), GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
AutoRemove: true,
NoSkipCheckout: true, NoSkipCheckout: true,
DisableActEnv: r.cfg.Runner.SetActEnv != nil && !*r.cfg.Runner.SetActEnv, DisableActEnv: r.cfg.Runner.SetActEnv != nil && !*r.cfg.Runner.SetActEnv,
PresetGitHubContext: preset, PresetGitHubContext: preset,
+3 -3
View File
@@ -44,7 +44,7 @@ func TestRunnerCleanupStaleTaskDirs(t *testing.T) {
now: func() time.Time { return now }, now: func() time.Time { return now },
} }
r.cleanupStaleTaskDirs(context.Background(), workdirRoot) r.cleanupStaleDirs(context.Background(), workdirRoot, isTaskIDDir)
assert.NoDirExists(t, oldTask) assert.NoDirExists(t, oldTask)
assert.DirExists(t, freshTask) assert.DirExists(t, freshTask)
@@ -111,7 +111,7 @@ func TestRunnerCleanupStaleTaskDirsMissingRoot(t *testing.T) {
// Must be a silent no-op rather than a warning or panic when the root // Must be a silent no-op rather than a warning or panic when the root
// has not yet been created (e.g. the runner has never executed a task). // has not yet been created (e.g. the runner has never executed a task).
r.cleanupStaleTaskDirs(context.Background(), filepath.Join(t.TempDir(), "missing")) r.cleanupStaleDirs(context.Background(), filepath.Join(t.TempDir(), "missing"), isTaskIDDir)
} }
func TestRunnerCleanupStaleTaskDirsHonorsContext(t *testing.T) { func TestRunnerCleanupStaleTaskDirsHonorsContext(t *testing.T) {
@@ -135,7 +135,7 @@ func TestRunnerCleanupStaleTaskDirsHonorsContext(t *testing.T) {
now: func() time.Time { return now }, now: func() time.Time { return now },
} }
r.cleanupStaleTaskDirs(ctx, workdirRoot) r.cleanupStaleDirs(ctx, workdirRoot, isTaskIDDir)
for i := 1001; i <= 1003; i++ { for i := 1001; i <= 1003; i++ {
assert.DirExists(t, filepath.Join(workdirRoot, strconv.Itoa(i))) assert.DirExists(t, filepath.Join(workdirRoot, strconv.Itoa(i)))
+4 -4
View File
@@ -83,7 +83,7 @@ func TestNewRunnerInitializesLabelsAndEnvironment(t *testing.T) {
Labels: []string{"ubuntu:host", "", "pool:e57e18d4"}, Labels: []string{"ubuntu:host", "", "pool:e57e18d4"},
} }
cli := clientmocks.NewClient(t) cli := clientmocks.NewClient(t)
cli.On("Address").Return("https://gitea.example/").Maybe() cli.AddressValue = "https://gitea.example/"
r := NewRunner(cfg, reg, cli) r := NewRunner(cfg, reg, cli)
@@ -137,7 +137,7 @@ func TestNewRunnerLeavesProxyToTheTask(t *testing.T) {
cfg.Cache.ExternalServer = "http://cache.local:8088/" cfg.Cache.ExternalServer = "http://cache.local:8088/"
reg := &config.Registration{Name: "runner"} reg := &config.Registration{Name: "runner"}
cli := clientmocks.NewClient(t) cli := clientmocks.NewClient(t)
cli.On("Address").Return("https://gitea.example/").Maybe() cli.AddressValue = "https://gitea.example/"
r := NewRunner(cfg, reg, cli) r := NewRunner(cfg, reg, cli)
@@ -161,7 +161,7 @@ func TestNewRunnerCacheServiceV2(t *testing.T) {
cfg := &config.Config{} cfg := &config.Config{}
cfg.Cache.Dir, cfg.Cache.Host = t.TempDir(), "127.0.0.1" cfg.Cache.Dir, cfg.Cache.Host = t.TempDir(), "127.0.0.1"
cli := clientmocks.NewClient(t) cli := clientmocks.NewClient(t)
cli.On("Address").Return("https://gitea.example/").Maybe() cli.AddressValue = "https://gitea.example/"
r := NewRunner(cfg, &config.Registration{Name: "runner"}, cli) r := NewRunner(cfg, &config.Registration{Name: "runner"}, cli)
t.Cleanup(func() { _ = r.Close() }) t.Cleanup(func() { _ = r.Close() })
@@ -202,7 +202,7 @@ func TestNewRunnerNormalizesTheExternalCacheServer(t *testing.T) {
cfg := &config.Config{} cfg := &config.Config{}
cfg.Cache.ExternalServer = "http://cache.local:8088//" cfg.Cache.ExternalServer = "http://cache.local:8088//"
cli := clientmocks.NewClient(t) cli := clientmocks.NewClient(t)
cli.On("Address").Return("https://gitea.example/").Maybe() cli.AddressValue = "https://gitea.example/"
r := NewRunner(cfg, &config.Registration{Name: "runner"}, cli) r := NewRunner(cfg, &config.Registration{Name: "runner"}, cli)
+8 -7
View File
@@ -4,16 +4,17 @@
package client package client
import ( import (
"gitea.dev/actionslib/ping/v1/pingv1connect" "context"
"gitea.dev/actionslib/runner/v1/runnerv1connect"
"connectrpc.com/connect"
"gitea.dev/actionslib/runner/v1"
) )
// A Client manages communication with the runner. // A Client manages communication with the runner.
//
//go:generate mockery --name Client
type Client interface { type Client interface {
pingv1connect.PingServiceClient
runnerv1connect.RunnerServiceClient
Address() string Address() string
Insecure() bool Declare(context.Context, *connect.Request[runnerv1.DeclareRequest]) (*connect.Response[runnerv1.DeclareResponse], error)
FetchTask(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error)
UpdateLog(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error)
UpdateTask(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error)
} }
-13
View File
@@ -1,13 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package client
import "gitea.dev/actionslib/pkg/protocol"
// The headers are defined in the shared protocol package so that Gitea and the
// runner cannot drift apart.
const (
UUIDHeader = protocol.UUIDHeader
TokenHeader = protocol.TokenHeader
)
+3 -8
View File
@@ -14,6 +14,7 @@ import (
"connectrpc.com/connect" "connectrpc.com/connect"
"gitea.dev/actionslib/ping/v1/pingv1connect" "gitea.dev/actionslib/ping/v1/pingv1connect"
"gitea.dev/actionslib/pkg/protocol"
"gitea.dev/actionslib/runner/v1/runnerv1connect" "gitea.dev/actionslib/runner/v1/runnerv1connect"
) )
@@ -42,10 +43,10 @@ func New(endpoint string, insecure bool, uuid, token string, timeout time.Durati
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
req.Header().Set("User-Agent", "gitea-runner/"+ver.Version()) req.Header().Set("User-Agent", "gitea-runner/"+ver.Version())
if uuid != "" { if uuid != "" {
req.Header().Set(UUIDHeader, uuid) req.Header().Set(protocol.UUIDHeader, uuid)
} }
if token != "" { if token != "" {
req.Header().Set(TokenHeader, token) req.Header().Set(protocol.TokenHeader, token)
} }
return next(ctx, req) return next(ctx, req)
} }
@@ -64,7 +65,6 @@ func New(endpoint string, insecure bool, uuid, token string, timeout time.Durati
opts..., opts...,
), ),
endpoint: endpoint, endpoint: endpoint,
insecure: insecure,
} }
} }
@@ -72,10 +72,6 @@ func (c *HTTPClient) Address() string {
return c.endpoint return c.endpoint
} }
func (c *HTTPClient) Insecure() bool {
return c.insecure
}
var _ Client = (*HTTPClient)(nil) var _ Client = (*HTTPClient)(nil)
// An HTTPClient manages communication with the runner API. // An HTTPClient manages communication with the runner API.
@@ -83,5 +79,4 @@ type HTTPClient struct {
pingv1connect.PingServiceClient pingv1connect.PingServiceClient
runnerv1connect.RunnerServiceClient runnerv1connect.RunnerServiceClient
endpoint string endpoint string
insecure bool
} }
+5 -5
View File
@@ -12,6 +12,7 @@ import (
"connectrpc.com/connect" "connectrpc.com/connect"
pingv1 "gitea.dev/actionslib/ping/v1" pingv1 "gitea.dev/actionslib/ping/v1"
"gitea.dev/actionslib/pkg/protocol"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -71,14 +72,13 @@ func TestNewSetsBaseURLAndHeaders(t *testing.T) {
c := New(server.URL+"/", false, "the-uuid", "the-token", time.Minute) c := New(server.URL+"/", false, "the-uuid", "the-token", time.Minute)
// Address returns the endpoint as supplied (untrimmed) // Address returns the endpoint as supplied (untrimmed)
require.Equal(t, server.URL+"/", c.Address()) require.Equal(t, server.URL+"/", c.Address())
require.False(t, c.Insecure())
// the call is expected to fail (server returns 500), we only assert what was sent // the call is expected to fail (server returns 500), we only assert what was sent
_, _ = c.Ping(t.Context(), connect.NewRequest(&pingv1.PingRequest{Data: "hi"})) _, _ = c.Ping(t.Context(), connect.NewRequest(&pingv1.PingRequest{Data: "hi"}))
require.True(t, strings.HasPrefix(gotPath, "/api/actions/"), "unexpected path %q", gotPath) require.True(t, strings.HasPrefix(gotPath, "/api/actions/"), "unexpected path %q", gotPath)
require.Equal(t, "the-uuid", gotHeaders.Get(UUIDHeader)) require.Equal(t, "the-uuid", gotHeaders.Get(protocol.UUIDHeader))
require.Equal(t, "the-token", gotHeaders.Get(TokenHeader)) require.Equal(t, "the-token", gotHeaders.Get(protocol.TokenHeader))
} }
func TestNewOmitsEmptyHeaders(t *testing.T) { func TestNewOmitsEmptyHeaders(t *testing.T) {
@@ -92,6 +92,6 @@ func TestNewOmitsEmptyHeaders(t *testing.T) {
c := New(server.URL, false, "", "", time.Minute) c := New(server.URL, false, "", "", time.Minute)
_, _ = c.Ping(t.Context(), connect.NewRequest(&pingv1.PingRequest{Data: "hi"})) _, _ = c.Ping(t.Context(), connect.NewRequest(&pingv1.PingRequest{Data: "hi"}))
require.Empty(t, gotHeaders.Get(UUIDHeader)) require.Empty(t, gotHeaders.Get(protocol.UUIDHeader))
require.Empty(t, gotHeaders.Get(TokenHeader)) require.Empty(t, gotHeaders.Get(protocol.TokenHeader))
} }
+36 -222
View File
@@ -1,251 +1,65 @@
// Code generated by mockery v2.42.1. DO NOT EDIT. // Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package mocks package mocks
import ( import (
context "context" "context"
"fmt"
connect "connectrpc.com/connect" "connectrpc.com/connect"
"gitea.dev/actionslib/runner/v1"
mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
pingv1 "gitea.dev/actionslib/ping/v1"
runnerv1 "gitea.dev/actionslib/runner/v1"
) )
// Client is an autogenerated mock type for the Client type
type Client struct { type Client struct {
mock.Mock mock.Mock
AddressValue string
} }
// Address provides a mock function with given fields: func (m *Client) Address() string {
func (_m *Client) Address() string { return m.AddressValue
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Address")
} }
var r0 string func call[Request, Response any](ctx context.Context, m *Client, method string, request *connect.Request[Request]) (*connect.Response[Response], error) {
if rf, ok := ret.Get(0).(func() string); ok { returns := m.MethodCalled(method, ctx, request)
r0 = rf() if callback, ok := returns.Get(0).(func(context.Context, *connect.Request[Request]) (*connect.Response[Response], error)); ok {
} else { return callback(ctx, request)
r0 = ret.Get(0).(string) }
var response *connect.Response[Response]
if value := returns.Get(0); value != nil {
var ok bool
response, ok = value.(*connect.Response[Response])
if !ok {
panic(fmt.Sprintf("unexpected response type %T for %s", value, method))
}
}
return response, returns.Error(1)
} }
return r0 func (m *Client) Declare(ctx context.Context, request *connect.Request[runnerv1.DeclareRequest]) (*connect.Response[runnerv1.DeclareResponse], error) {
return call[runnerv1.DeclareRequest, runnerv1.DeclareResponse](ctx, m, "Declare", request)
} }
// Declare provides a mock function with given fields: _a0, _a1 func (m *Client) FetchTask(ctx context.Context, request *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error) {
func (_m *Client) Declare(_a0 context.Context, _a1 *connect.Request[runnerv1.DeclareRequest]) (*connect.Response[runnerv1.DeclareResponse], error) { return call[runnerv1.FetchTaskRequest, runnerv1.FetchTaskResponse](ctx, m, "FetchTask", request)
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for Declare")
} }
var r0 *connect.Response[runnerv1.DeclareResponse] func (m *Client) UpdateLog(ctx context.Context, request *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error) {
var r1 error return call[runnerv1.UpdateLogRequest, runnerv1.UpdateLogResponse](ctx, m, "UpdateLog", request)
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.DeclareRequest]) (*connect.Response[runnerv1.DeclareResponse], error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.DeclareRequest]) *connect.Response[runnerv1.DeclareResponse]); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connect.Response[runnerv1.DeclareResponse])
}
} }
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.DeclareRequest]) error); ok { func (m *Client) UpdateTask(ctx context.Context, request *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error) {
r1 = rf(_a0, _a1) return call[runnerv1.UpdateTaskRequest, runnerv1.UpdateTaskResponse](ctx, m, "UpdateTask", request)
} else {
r1 = ret.Error(1)
} }
return r0, r1
}
// FetchTask provides a mock function with given fields: _a0, _a1
func (_m *Client) FetchTask(_a0 context.Context, _a1 *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for FetchTask")
}
var r0 *connect.Response[runnerv1.FetchTaskResponse]
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) *connect.Response[runnerv1.FetchTaskResponse]); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connect.Response[runnerv1.FetchTaskResponse])
}
}
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Insecure provides a mock function with given fields:
func (_m *Client) Insecure() bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Insecure")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// Ping provides a mock function with given fields: _a0, _a1
func (_m *Client) Ping(_a0 context.Context, _a1 *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for Ping")
}
var r0 *connect.Response[pingv1.PingResponse]
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[pingv1.PingRequest]) *connect.Response[pingv1.PingResponse]); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connect.Response[pingv1.PingResponse])
}
}
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[pingv1.PingRequest]) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Register provides a mock function with given fields: _a0, _a1
func (_m *Client) Register(_a0 context.Context, _a1 *connect.Request[runnerv1.RegisterRequest]) (*connect.Response[runnerv1.RegisterResponse], error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for Register")
}
var r0 *connect.Response[runnerv1.RegisterResponse]
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.RegisterRequest]) (*connect.Response[runnerv1.RegisterResponse], error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.RegisterRequest]) *connect.Response[runnerv1.RegisterResponse]); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connect.Response[runnerv1.RegisterResponse])
}
}
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.RegisterRequest]) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateLog provides a mock function with given fields: _a0, _a1
func (_m *Client) UpdateLog(_a0 context.Context, _a1 *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for UpdateLog")
}
var r0 *connect.Response[runnerv1.UpdateLogResponse]
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) *connect.Response[runnerv1.UpdateLogResponse]); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connect.Response[runnerv1.UpdateLogResponse])
}
}
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateTask provides a mock function with given fields: _a0, _a1
func (_m *Client) UpdateTask(_a0 context.Context, _a1 *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for UpdateTask")
}
var r0 *connect.Response[runnerv1.UpdateTaskResponse]
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) *connect.Response[runnerv1.UpdateTaskResponse]); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connect.Response[runnerv1.UpdateTaskResponse])
}
}
if rf, ok := ret.Get(1).(func(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewClient(t interface { func NewClient(t interface {
mock.TestingT mock.TestingT
Cleanup(func()) Cleanup(func())
}, },
) *Client { ) *Client {
mock := &Client{} client := &Client{}
mock.Mock.Test(t) client.Test(t)
t.Cleanup(func() { client.AssertExpectations(t) })
t.Cleanup(func() { mock.AssertExpectations(t) }) return client
return mock
} }