mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-05-07 15:53:24 +02:00
Audit-driven cleanup of `act/` test fixtures. Three commits:
**1. Remove dead fixtures** — 12 fixture directories that no Go test references: `dir with spaces`, `environment-variables`, `issue-104`, `issue-122`, `issue-141`, `localdockerimagetest_`, `node`, `parallel`, `python`, `uses-composite-with-inputs`, `uses-composite-with-pre-and-post-steps`, `shells/custom` (under `act/runner/testdata/`), plus `act/artifactcache/testdata/example`.
**2. Collapse `actions/node{12,16,20}` to a single `actions/node24` fixture** — the trio dispatched through identical `IsNode()` code paths and exercised the container's node binary, not the `using:` string. Bumps bundled deps to current (`@actions/core@^3`, `@actions/github@^9`, `@vercel/ncc@^0.38.4`) — both runtime packages are now ESM-only, so `index.js` is rewritten to ESM and `"type": "module"` added. Drops committed `node_modules/` and `package-lock.json` (now gitignored locally; `dist/` continues to be ignored by the repo-root `.gitignore` as before). Reduces `local-action-js/push.yml` to a single `test-node24` job and bumps four other stale `using: node12/16` references in fixtures.
**3. Bump test container base images** to `node:24-bookworm-slim` / `node:24-bookworm` / `ubuntu:24.04`. Replaces `node:16-buster-slim`, `node:16-buster`, `node:12.20.1-buster-slim`, and the EOL `node:12-buster-slim` / `node:16-buster-slim` / `ubuntu:18.04` base images in `actions/{docker-local,docker-local-noargs,action1}/Dockerfile`.
The runner's model still accepts `using: node12/16/20` for third-party actions in the wild — those constants are untouched.
Fixes: https://gitea.com/gitea/runner/issues/931
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/runner/pulls/932
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
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/runner/act/model"
|
|
"gitea.com/gitea/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.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))
|
|
}
|
|
|
|
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:24-bookworm", // Don't use node:24-bookworm-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.NoError(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.NoError(t, err, tjfi.workflowPath) //nolint:testifylint // pre-existing issue from nektos/act
|
|
|
|
planner, err := model.NewWorkflowPlanner(fullWorkflowPath, true)
|
|
assert.NoError(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.NoError(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))
|
|
}
|