mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-04-24 12:50:31 +08:00
Merges the `gitea.com/gitea/act` fork into this repository as the `act/` directory and consumes it as a local package. The `replace github.com/nektos/act => gitea.com/gitea/act` directive is removed; act's dependencies are merged into the root `go.mod`. - Imports rewritten: `github.com/nektos/act/pkg/...` → `gitea.com/gitea/act_runner/act/...` (flattened — `pkg/` boundary dropped to match the layout forgejo-runner adopted). - Dropped act's CLI (`cmd/`, `main.go`) and all upstream project files; kept the library tree + `LICENSE`. - Added `// Copyright <year> The Gitea Authors ...` / `// Copyright <year> nektos` headers to 104 `.go` files. - Pre-existing act lint violations annotated inline with `//nolint:<linter> // pre-existing issue from nektos/act`. `.golangci.yml` is unchanged vs `main`. - Makefile test target: `-race -short` (matches forgejo-runner). - Pre-existing integration test failures fixed: race in parallel executor (atomic counters); TestSetupEnv / command_test / expression_test / run_context_test updated to match gitea fork runtime; TestJobExecutor and TestActionCache gated on `testing.Short()`. Full `gitea/act` commit history is reachable via the second parent. Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
400 lines
10 KiB
Go
400 lines
10 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// Copyright 2021 The nektos/act Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package artifacts
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"gitea.com/gitea/act_runner/act/model"
|
|
"gitea.com/gitea/act_runner/act/runner"
|
|
|
|
"github.com/julienschmidt/httprouter"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
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.Equal(1, len(response.Value)) //nolint:testifylint // pre-existing issue from nektos/act
|
|
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))
|
|
}
|
|
|
|
type TestJobFileInfo struct {
|
|
workdir string
|
|
workflowPath string
|
|
eventName string
|
|
errorMessage string
|
|
platforms map[string]string
|
|
containerArchitecture string
|
|
}
|
|
|
|
var (
|
|
artifactsPath = path.Join(os.TempDir(), "test-artifacts")
|
|
artifactsAddr = "127.0.0.1"
|
|
artifactsPort = "12345"
|
|
)
|
|
|
|
func TestArtifactFlow(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
cancel := Serve(ctx, artifactsPath, artifactsAddr, artifactsPort)
|
|
defer cancel()
|
|
|
|
platforms := map[string]string{
|
|
"ubuntu-latest": "node:16-buster", // Don't use node:16-buster-slim because it doesn't have curl command, which is used in the tests
|
|
}
|
|
|
|
tables := []TestJobFileInfo{
|
|
{"testdata", "upload-and-download", "push", "", platforms, ""},
|
|
{"testdata", "GHSL-2023-004", "push", "", platforms, ""},
|
|
}
|
|
log.SetLevel(log.DebugLevel)
|
|
|
|
for _, table := range tables {
|
|
runTestJobFile(ctx, t, table)
|
|
}
|
|
}
|
|
|
|
func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
|
|
t.Run(tjfi.workflowPath, func(t *testing.T) {
|
|
fmt.Printf("::group::%s\n", tjfi.workflowPath) //nolint:forbidigo // pre-existing issue from nektos/act
|
|
|
|
if err := os.RemoveAll(artifactsPath); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
workdir, err := filepath.Abs(tjfi.workdir)
|
|
assert.Nil(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
|
|
fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath)
|
|
runnerConfig := &runner.Config{
|
|
Workdir: workdir,
|
|
BindWorkdir: false,
|
|
EventName: tjfi.eventName,
|
|
Platforms: tjfi.platforms,
|
|
ReuseContainers: false,
|
|
ContainerArchitecture: tjfi.containerArchitecture,
|
|
GitHubInstance: "github.com",
|
|
ArtifactServerPath: artifactsPath,
|
|
ArtifactServerAddr: artifactsAddr,
|
|
ArtifactServerPort: artifactsPort,
|
|
}
|
|
|
|
runner, err := runner.New(runnerConfig)
|
|
assert.Nil(t, err, tjfi.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
|
|
|
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
|
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
|
|
|
plan, err := planner.PlanEvent(tjfi.eventName)
|
|
if err == nil {
|
|
err = runner.NewPlanExecutor(plan)(ctx)
|
|
if tjfi.errorMessage == "" {
|
|
assert.Nil(t, err, fullWorkflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
|
} else {
|
|
assert.Error(t, err, tjfi.errorMessage) //nolint:testifylint // pre-existing issue from nektos/act
|
|
}
|
|
} else {
|
|
assert.Nil(t, plan)
|
|
}
|
|
|
|
fmt.Println("::endgroup::") //nolint:forbidigo // pre-existing issue from nektos/act
|
|
})
|
|
}
|
|
|
|
func TestMkdirFsImplSafeResolve(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
baseDir := "/foo/bar"
|
|
|
|
tests := map[string]struct {
|
|
input string
|
|
want string
|
|
}{
|
|
"simple": {input: "baz", want: "/foo/bar/baz"},
|
|
"nested": {input: "baz/blue", want: "/foo/bar/baz/blue"},
|
|
"dots in middle": {input: "baz/../../blue", want: "/foo/bar/blue"},
|
|
"leading dots": {input: "../../parent", want: "/foo/bar/parent"},
|
|
"root path": {input: "/root", want: "/foo/bar/root"},
|
|
"root": {input: "/", want: "/foo/bar"},
|
|
"empty": {input: "", want: "/foo/bar"},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert.Equal(tc.want, safeResolve(baseDir, tc.input))
|
|
})
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|