mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-10-30 03:46:33 +08:00
Without a docker daemon six `act/runner` tests failed instead of passing. A dry run still created and removed job networks, the job teardown dropped the dry-run flag along with every other context value, and the container mocks asked the daemon for the runner arch. This also speeds up the tests and CI without dropping assertions. Most of the time went to git and docker round trips, which are now pinned, batched or coalesced, and the dind and e2e steps cache or overlap more of their setup. On CI, `act/runner` went from 77s to 55s and `test-dind` from 108s to 55s. Locally, `act/runner` without docker went from 53s to 7s. Split out of https://gitea.com/gitea/runner/pulls/1348, which adds Windows CI on top. The go cache changes moved to https://gitea.com/gitea/runner/pulls/1350. Written by Claude Code. --------- Co-authored-by: bircni <bircni@icloud.com> Reviewed-on: https://gitea.com/gitea/runner/pulls/1349 Reviewed-by: bircni <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io>
222 lines
8.5 KiB
Go
222 lines
8.5 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package runner
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.com/gitea/runner/act/common/git"
|
|
|
|
"gitea.dev/actionslib/pkg/model"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Regression test for go-gitea/gitea#37483: a remote reusable workflow at a moving
|
|
// ref (branch/tag) must reflect the new tip on every invocation, not stay pinned
|
|
// to the cache populated on the first run.
|
|
func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
|
|
t.Parallel()
|
|
if _, err := exec.LookPath("git"); err != nil {
|
|
t.Skip("git not available in PATH")
|
|
}
|
|
|
|
remoteDir := t.TempDir()
|
|
gitMust(t, "", "init", "--bare", "--initial-branch=master", remoteDir)
|
|
|
|
workDir := t.TempDir()
|
|
gitMust(t, "", "clone", remoteDir, workDir)
|
|
gitMust(t, workDir, "config", "user.email", "test@test")
|
|
gitMust(t, workDir, "config", "user.name", "test")
|
|
gitMust(t, workDir, "checkout", "-b", "master")
|
|
|
|
const workflowPath = ".gitea/workflows/reusable.yml"
|
|
tmpl := func(tag string) string {
|
|
return "name: reusable\non:\n workflow_call:\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo " + tag + "\n"
|
|
}
|
|
|
|
require.NoError(t, os.MkdirAll(filepath.Join(workDir, ".gitea/workflows"), 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(workDir, workflowPath), []byte(tmpl("v1")), 0o644))
|
|
gitMust(t, workDir, "add", workflowPath)
|
|
gitMust(t, workDir, "commit", "-m", "v1")
|
|
gitMust(t, workDir, "push", "-u", "origin", "master")
|
|
|
|
rc := &RunContext{
|
|
Config: &Config{Env: testCheckoutEnv()},
|
|
Run: &model.Run{
|
|
JobID: "j1",
|
|
Workflow: &model.Workflow{
|
|
Name: "wf",
|
|
Jobs: map[string]*model.Job{"j1": {}},
|
|
},
|
|
},
|
|
}
|
|
cacheDir := t.TempDir()
|
|
|
|
require.NoError(t, cloneRemoteReusableWorkflow(rc, remoteDir, "master", cacheDir)(t.Context()))
|
|
got, err := os.ReadFile(filepath.Join(cacheDir, workflowPath))
|
|
require.NoError(t, err)
|
|
require.Equal(t, tmpl("v1"), string(got))
|
|
|
|
// Branch tip moves; cache key (cacheDir) does not.
|
|
require.NoError(t, os.WriteFile(filepath.Join(workDir, workflowPath), []byte(tmpl("v2")), 0o644))
|
|
gitMust(t, workDir, "commit", "-am", "v2")
|
|
gitMust(t, workDir, "push", "origin", "master")
|
|
|
|
require.NoError(t, cloneRemoteReusableWorkflow(rc, remoteDir, "master", cacheDir)(t.Context()))
|
|
got, err = os.ReadFile(filepath.Join(cacheDir, workflowPath))
|
|
require.NoError(t, err)
|
|
require.Equal(t, tmpl("v2"), string(got), "cached workflow file must reflect the updated branch tip")
|
|
}
|
|
|
|
func TestNewReusableWorkflowExecutorHoldsCloneLock(t *testing.T) {
|
|
workflowDir := t.TempDir()
|
|
require.NoError(t, os.WriteFile(filepath.Join(workflowDir, "reusable.yml"), []byte(":"), 0o644))
|
|
|
|
unlock, err := git.AcquireCloneLock(t.Context(), workflowDir)
|
|
require.NoError(t, err)
|
|
unlockOnce := sync.OnceFunc(unlock)
|
|
defer unlockOnce()
|
|
|
|
rc := &RunContext{
|
|
Config: &Config{},
|
|
Run: &model.Run{Workflow: &model.Workflow{Jobs: map[string]*model.Job{}}},
|
|
}
|
|
exec := newReusableWorkflowExecutor(rc, workflowDir, "reusable.yml", true)
|
|
|
|
done := make(chan error, 1)
|
|
go func() { done <- exec(context.Background()) }()
|
|
|
|
select {
|
|
case err := <-done:
|
|
t.Fatalf("executor returned while clone lock was held: %v", err)
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
unlockOnce()
|
|
|
|
select {
|
|
case err := <-done:
|
|
require.Error(t, err)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("executor did not return after lock was released")
|
|
}
|
|
}
|
|
|
|
func TestNewLocalReusableWorkflowExecutorFindsSameRepositoryPaths(t *testing.T) {
|
|
workdir := filepath.Join(t.TempDir(), "workspace")
|
|
require.NoError(t, os.MkdirAll(filepath.Join(workdir, ".gitea", "workflows"), 0o755))
|
|
require.NoError(t, os.Mkdir(workdir+".lock", 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(workdir, ".gitea", "workflows", "reusable.yml"), []byte(":"), 0o644))
|
|
|
|
for _, uses := range []string{"./.gitea/workflows/reusable.yml", "$/.gitea/workflows/reusable.yml"} {
|
|
rc := &RunContext{
|
|
Config: &Config{Workdir: workdir},
|
|
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {Uses: uses}}}},
|
|
}
|
|
err := newLocalReusableWorkflowExecutor(rc)(t.Context())
|
|
require.ErrorContains(t, err, "workflow is not valid")
|
|
}
|
|
}
|
|
|
|
func TestCloneRemoteReusableWorkflowSendsInstanceTokenAndClientCert(t *testing.T) {
|
|
var gotCert bool
|
|
var gotToken string
|
|
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotCert = len(r.TLS.PeerCertificates) > 0
|
|
_, gotToken, _ = r.BasicAuth()
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
server.TLS = &tls.Config{ClientAuth: tls.RequireAnyClientCert}
|
|
server.StartTLS()
|
|
defer server.Close()
|
|
pair := server.TLS.Certificates[0]
|
|
keyDER, err := x509.MarshalPKCS8PrivateKey(pair.PrivateKey)
|
|
require.NoError(t, err)
|
|
dir := t.TempDir()
|
|
certFile, keyFile := filepath.Join(dir, "client.crt"), filepath.Join(dir, "client.key")
|
|
require.NoError(t, os.WriteFile(certFile, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: pair.Certificate[0]}), 0o600))
|
|
require.NoError(t, os.WriteFile(keyFile, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), 0o600))
|
|
rc := &RunContext{
|
|
Config: &Config{
|
|
Env: testCheckoutEnv(),
|
|
GitHubInstance: server.URL,
|
|
Secrets: map[string]string{"GITEA_TOKEN": "token"},
|
|
InsecureSkipTLS: true,
|
|
ClientCertFile: certFile,
|
|
ClientKeyFile: keyFile,
|
|
},
|
|
Run: &model.Run{JobID: "j1", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"j1": {}}}},
|
|
}
|
|
|
|
require.Error(t, cloneRemoteReusableWorkflow(rc, server.URL+"/owner/repo", "main", t.TempDir())(t.Context()))
|
|
require.True(t, gotCert)
|
|
require.Equal(t, "token", gotToken)
|
|
}
|
|
|
|
func TestInstanceAuthAndTransportOnlyApplyToTheInstance(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name, instanceURL, trustedActionInstance, cloneURL string
|
|
token, cert, insecure bool
|
|
}{
|
|
{"same host with schemaless instance", "gitea.example.net", "", "https://gitea.example.net/actions/tools", true, true, true},
|
|
{"same host with schemaless instance and port", "gitea.example.net:3000", "", "https://gitea.example.net:3000/actions/tools", true, true, true},
|
|
{"different host", "gitea.example.net", "", "https://github.com/actions/tools", false, false, false},
|
|
{"embedded basic auth keeps its own credentials", "gitea.example.net", "", "https://user:pass@gitea.example.net/actions/tools", false, true, true},
|
|
{"invalid clone URL", "gitea.example.net", "", "://gitea.example.net/actions/tools", false, false, false},
|
|
{"self-hosted action instance on different host skips no verification", "gitea.local", "https://gitea.my-nas.lan", "https://gitea.my-nas.lan/owner/action", true, true, false},
|
|
{"self-hosted action instance with embedded basic auth", "gitea.local", "https://gitea.my-nas.lan", "https://user:pass@gitea.my-nas.lan/owner/action", false, true, false},
|
|
{"github mode does not trust mirror host", "gitea.local", "", "https://mirror.example.com/owner/action", false, false, false},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
conf := Config{
|
|
GitHubInstance: tt.instanceURL,
|
|
DefaultActionInstance: tt.trustedActionInstance,
|
|
DefaultActionInstanceIsSelfHosted: tt.trustedActionInstance != "",
|
|
InsecureSkipTLS: true,
|
|
ClientCertFile: "/cert",
|
|
ClientKeyFile: "/key",
|
|
InstanceTransport: &http.Transport{},
|
|
}
|
|
want := git.NewGitCloneExecutorInput{URL: tt.cloneURL, InsecureSkipTLS: tt.insecure}
|
|
if tt.insecure {
|
|
want.Transport = conf.InstanceTransport
|
|
}
|
|
if tt.token {
|
|
want.Token = "token"
|
|
}
|
|
if tt.cert {
|
|
want.ClientCertFile, want.ClientKeyFile = conf.ClientCertFile, conf.ClientKeyFile
|
|
}
|
|
require.Equal(t, want, conf.withInstanceAuth(git.NewGitCloneExecutorInput{URL: tt.cloneURL}, "token"))
|
|
})
|
|
}
|
|
|
|
transport := &http.Transport{}
|
|
conf := Config{GitHubInstance: "https://gitea.local", InstanceTransport: transport}
|
|
require.Same(t, transport, conf.instanceTransportFor("https://gitea.local/api/actions_pipeline/"))
|
|
require.Nil(t, conf.instanceTransportFor("https://gitea.local.example/api/actions_pipeline/"))
|
|
require.Nil(t, conf.instanceTransportFor("https://other.example/x#/api/actions_pipeline/"))
|
|
}
|
|
|
|
func gitMust(t *testing.T, dir string, args ...string) {
|
|
t.Helper()
|
|
cmd := exec.Command("git", args...)
|
|
if dir != "" {
|
|
cmd.Dir = dir
|
|
}
|
|
out, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, "git %v: %s", args, string(out))
|
|
}
|