mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-06-08 14:36:35 +00:00
Running the full suite under `-race` (dropping `-short`) exposed pre-existing data races in parallel matrix-job execution, fixed by not sharing mutable state across combinations: - `containerDaemonSocket()`/`validVolumes()` derive per-job values instead of mutating shared `Config` - `getWorkflowSecrets` builds a fresh map, `rc.steps()` clones each step, and go-git workdir access is serialized - every write to a shared `Job`'s result/outputs runs under a per-`Job` lock, each combo interpolating outputs from a pristine snapshot (last wins, as on GitHub) ### Test suite - capability gates (docker / network / host-tools / Linux) replace the `-short` skips, and the suite runs offline via local fixtures (the artifact flow uses an in-process loopback server, only the docker-action force-pull needs the network) - drops redundant tests, adds a regression test for https://gitea.com/gitea/runner/issues/981 and a docker-in-docker harness (`make test-dind`) --- This PR was written with the help of Claude Opus 4.7 Reviewed-on: https://gitea.com/gitea/runner/pulls/994 Reviewed-by: Nicolas <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io> Co-committed-by: silverwind <me@silverwind.io>
158 lines
5.7 KiB
Go
158 lines
5.7 KiB
Go
// 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.com/gitea/runner/act/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)
|
|
})
|
|
}
|
|
}
|