mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-08 00:00:28 +00:00
enhance: test on Windows and fix what it found (#1348)
Adds a weekly and on-demand workflow that runs the cross-compiled tests on Windows Server 2025 Core in a [dockur/windows](https://github.com/dockur/windows) QEMU VM, and fixes the Windows bugs it found in host and UNC paths, env handling, shell quoting, post-task scripts, health checks, checkout, action symlinks, step summaries, Git client certificates and docker mode from a Windows host. Host-mode steps now emit UTF-8, `bash` falls back to Git's bash instead of the WSL launcher, the runner can run as a native Windows service, steps can keep background helpers with `RUNNER_TRACKING_ID`, and Windows arm64 binaries are released. Fixes: https://gitea.com/gitea/runner/issues/1339 Fixes: https://gitea.com/gitea/runner/issues/637 Fixes: https://gitea.com/gitea/runner/issues/1172 Fixes: https://gitea.com/gitea/runner/issues/347 Reviewed-on: https://gitea.com/gitea/runner/pulls/1348 Reviewed-by: bircni <[email protected]>
This commit is contained in:
1 parent
6e42621b4a
commit
d6f5b68a0d
85 files changed
+2578
-620
No files matched your search
@@ -42,7 +42,10 @@ func Main(ctx context.Context, c *action.Context) (err error) {
|
||||
return fmt.Errorf("checkout: unsupported input %q", name)
|
||||
}
|
||||
}
|
||||
dst, err := workspaceDestination(c.Workspace, c.Inputs["path"])
|
||||
runnerContext := c.Container.GetRunnerContext(ctx)
|
||||
executionOS, _ := runnerContext["os"].(string)
|
||||
windows := strings.EqualFold(executionOS, "windows")
|
||||
dst, err := workspaceDestination(c.Workspace, c.Inputs["path"], windows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -63,8 +66,8 @@ func Main(ctx context.Context, c *action.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if output := c.Env["GITHUB_OUTPUT"]; output != "" {
|
||||
return c.Container.Copy(path.Dir(output), &container.FileEntry{Name: path.Base(output), Mode: 0o666, Body: fmt.Sprintf("ref=%s\ncommit=%s\n", ghc.Ref, ghc.Sha)})(ctx)
|
||||
if output := filepath.ToSlash(c.Env["GITHUB_OUTPUT"]); output != "" {
|
||||
return c.Container.Copy(container.JoinPath(output, ".."), &container.FileEntry{Name: path.Base(output), Mode: 0o666, Body: fmt.Sprintf("ref=%s\ncommit=%s\n", ghc.Ref, ghc.Sha)})(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -72,6 +75,7 @@ func Main(ctx context.Context, c *action.Context) (err error) {
|
||||
co := &checkout{
|
||||
url: strings.TrimSuffix(ghc.ServerURL, "/") + "/" + repository,
|
||||
dst: dst,
|
||||
windows: windows,
|
||||
ref: ref,
|
||||
depth: depth,
|
||||
fetchTags: boolInput(c.Inputs, "fetch-tags", false),
|
||||
@@ -114,10 +118,11 @@ func Main(ctx context.Context, c *action.Context) (err error) {
|
||||
}
|
||||
|
||||
// named like actions/checkout's, which create-pull-request knows to hide, and gone with the job
|
||||
runnerTemp, _ := c.Container.GetRunnerContext(ctx)["temp"].(string)
|
||||
runnerTemp, _ := runnerContext["temp"].(string)
|
||||
runnerTemp = filepath.ToSlash(runnerTemp)
|
||||
digest := sha256.Sum256([]byte(dst))
|
||||
credentials := fmt.Sprintf("git-credentials-%x.config", digest[:8])
|
||||
co.credentials = path.Join(runnerTemp, credentials)
|
||||
co.credentials = container.JoinPath(runnerTemp, credentials)
|
||||
persist := boolInput(c.Inputs, "persist-credentials", true)
|
||||
config, remoteConfig := serverConfig(ghc.ServerURL, cmp.Or(c.Inputs["token"], ghc.Token), c.InsecureSkipTLS, co.submodules != "" && sshKey == "")
|
||||
if boolInput(c.Inputs, "set-safe-directory", true) {
|
||||
@@ -140,22 +145,21 @@ func Main(ctx context.Context, c *action.Context) (err error) {
|
||||
if err := c.Container.Copy(runnerTemp, &container.FileEntry{Name: cert, Mode: 0o600, Body: string(certPEM)}, &container.FileEntry{Name: key, Mode: 0o600, Body: string(keyPEM)})(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
scope := "http." + strings.TrimSuffix(ghc.ServerURL, "/") + "/."
|
||||
remoteConfig = append(remoteConfig, scope+"sslCert="+path.Join(runnerTemp, cert), scope+"sslKey="+path.Join(runnerTemp, key))
|
||||
remoteConfig = append(remoteConfig, git.ClientCertConfig("http."+strings.TrimSuffix(ghc.ServerURL, "/")+"/.", container.JoinPath(runnerTemp, cert), container.JoinPath(runnerTemp, key), executionOS)...)
|
||||
}
|
||||
env := gitEnv(c, remoteConfig)
|
||||
env["GIT_LFS_SKIP_SMUDGE"] = strconv.FormatBool(!co.lfs || len(co.sparse) == 0) // lfs pull fetches the objects of a full checkout in one go
|
||||
if sshKey != "" {
|
||||
key := fmt.Sprintf("git-ssh-key-%x", digest[:8])
|
||||
files := []*container.FileEntry{{Name: key, Mode: 0o600, Body: sshKey + "\n"}}
|
||||
ssh := []string{"ssh", "-i", path.Join(runnerTemp, key)}
|
||||
ssh := []string{"ssh", "-i", container.JoinPath(runnerTemp, key)}
|
||||
if boolInput(c.Inputs, "ssh-strict", true) {
|
||||
ssh = append(ssh, "-o", "StrictHostKeyChecking=yes", "-o", "CheckHostIP=no")
|
||||
}
|
||||
if knownHosts := strings.TrimSpace(c.Inputs["ssh-known-hosts"]); knownHosts != "" {
|
||||
name := fmt.Sprintf("git-ssh-known-hosts-%x", digest[:8])
|
||||
files = append(files, &container.FileEntry{Name: name, Mode: 0o600, Body: knownHosts + "\n"})
|
||||
ssh = append(ssh, "-o", "UserKnownHostsFile=~/.ssh/known_hosts "+strconv.Quote(path.Join(runnerTemp, name)))
|
||||
ssh = append(ssh, "-o", "UserKnownHostsFile=~/.ssh/known_hosts "+strconv.Quote(container.JoinPath(runnerTemp, name)))
|
||||
}
|
||||
if !persist {
|
||||
defer func() {
|
||||
@@ -218,7 +222,7 @@ func gitEnv(c *action.Context, config []string) map[string]string {
|
||||
env := map[string]string{"GIT_TERMINAL_PROMPT": "0"}
|
||||
maps.Copy(env, c.Env)
|
||||
maps.DeleteFunc(env, func(name, _ string) bool { return slices.Contains(git.LocalEnvVars, name) })
|
||||
maps.Copy(env, git.ConfigEnv(append([]string{"protocol.version=2", "init.defaultBranch=main", "advice.detachedHead=false", "maintenance.auto=false"}, config...)...))
|
||||
maps.Copy(env, git.ConfigEnv(append([]string{"protocol.version=2", "init.defaultBranch=main", "advice.detachedHead=false", "maintenance.auto=false", "core.longpaths=true"}, config...)...))
|
||||
return env
|
||||
}
|
||||
|
||||
@@ -301,7 +305,7 @@ type checkout struct {
|
||||
filter, submodules string
|
||||
sparse []string
|
||||
depth int
|
||||
fetchTags, clean, lfs, progress, cone bool
|
||||
fetchTags, clean, lfs, progress, cone, windows bool
|
||||
}
|
||||
|
||||
const fullDepth = "2147483647" // git's INFINITE_DEPTH, also deepens a shallow dst
|
||||
@@ -315,6 +319,9 @@ func (co *checkout) commands() (commands [][]string, fetched int) {
|
||||
repo("config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"),
|
||||
repo("config", "--replace-all", "--fixed-value", credentialsInclude, co.credentials, co.credentials),
|
||||
}
|
||||
if co.windows {
|
||||
commands = append(commands, repo("config", "core.longpaths", "true")) // later steps run git on the checkout too
|
||||
}
|
||||
if co.lfs {
|
||||
commands = append(commands, repo("lfs", "install", "--local"))
|
||||
}
|
||||
@@ -451,9 +458,13 @@ func boolInput(inputs map[string]string, name string, fallback bool) bool {
|
||||
return strings.EqualFold(value, "true") || value == "" && fallback
|
||||
}
|
||||
|
||||
func workspaceDestination(workspace, checkoutPath string) (string, error) {
|
||||
cleanPath := path.Clean(strings.TrimSpace(checkoutPath))
|
||||
if path.IsAbs(cleanPath) || cleanPath == ".." || strings.HasPrefix(cleanPath, "../") {
|
||||
func workspaceDestination(workspace, checkoutPath string, windows bool) (string, error) {
|
||||
cleanPath := strings.TrimSpace(checkoutPath)
|
||||
if windows {
|
||||
cleanPath = strings.ReplaceAll(cleanPath, `\`, "/")
|
||||
}
|
||||
cleanPath = path.Clean(cleanPath)
|
||||
if path.IsAbs(cleanPath) || cleanPath == ".." || strings.HasPrefix(cleanPath, "../") || windows && len(cleanPath) > 1 && cleanPath[1] == ':' {
|
||||
return "", fmt.Errorf("checkout: path %q must be relative to the workspace", checkoutPath)
|
||||
}
|
||||
return path.Join(workspace, cleanPath), nil
|
||||
|
||||
@@ -13,9 +13,11 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -50,6 +52,7 @@ func gitLog(dir string) []string {
|
||||
|
||||
func TestCheckout(t *testing.T) {
|
||||
server := t.TempDir()
|
||||
serverURL := (&url.URL{Scheme: "file", Path: "/" + strings.TrimPrefix(filepath.ToSlash(server), "/")}).String()
|
||||
var sha string
|
||||
_, lfsErr := exec.LookPath("git-lfs")
|
||||
require.True(t, t.Run("fixtures", func(t *testing.T) {
|
||||
@@ -192,13 +195,16 @@ exec sh -c "$(printf '%s' "$last" | sed "s#git-upload-pack '/\(.*\)\.git'#git up
|
||||
if tc.inputs["lfs"] == "true" && lfsErr != nil {
|
||||
t.Skip("needs git-lfs")
|
||||
}
|
||||
if tc.inputs["ssh-key"] != "" && runtime.GOOS == "windows" {
|
||||
t.Skip("Git for Windows puts its own ssh ahead of the fake one on PATH")
|
||||
}
|
||||
workspace, runnerTemp := t.TempDir(), t.TempDir()
|
||||
output := filepath.Join(runnerTemp, "output")
|
||||
checkout := func(inputs map[string]string) error {
|
||||
return Main(t.Context(), &action.Context{
|
||||
Container: &container.HostEnvironment{Path: workspace, TmpDir: runnerTemp, StdOut: io.Discard},
|
||||
Github: &model.GithubContext{
|
||||
Repository: "org/repo", Ref: cmp.Or(tc.eventRef, "refs/heads/main"), Sha: sha, ServerURL: "file://" + server, Token: "tok", Event: tc.event,
|
||||
Repository: "org/repo", Ref: cmp.Or(tc.eventRef, "refs/heads/main"), Sha: sha, ServerURL: serverURL, Token: "tok", Event: tc.event,
|
||||
},
|
||||
Inputs: inputs,
|
||||
HostWorkdir: host,
|
||||
@@ -207,7 +213,7 @@ exec sh -c "$(printf '%s' "$last" | sed "s#git-upload-pack '/\(.*\)\.git'#git up
|
||||
NoSkipCheckout: tc.noSkip,
|
||||
Env: map[string]string{
|
||||
"PATH": tools + string(os.PathListSeparator) + os.Getenv("PATH"), "GIT_CONFIG_GLOBAL": globalConfig, "GIT_CONFIG_NOSYSTEM": "1",
|
||||
"GIT_DIR": host, "GITHUB_OUTPUT": output, "SSH_SERVER": server, "SSH_ARGS": filepath.Join(runnerTemp, "ssh-args"),
|
||||
"GIT_DIR": host, "GITHUB_OUTPUT": output, "SSH_SERVER": filepath.ToSlash(server), "SSH_ARGS": filepath.Join(runnerTemp, "ssh-args"),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -270,7 +276,7 @@ exec sh -c "$(printf '%s' "$last" | sed "s#git-upload-pack '/\(.*\)\.git'#git up
|
||||
for _, repo := range repos {
|
||||
assert.Equal(t, tc.inputs["clean"] == "false", fileExists(filepath.Join(repo, "untracked")), repo)
|
||||
assert.Len(t, strings.Fields(gitOutput(repo, "config", "--local", "--get-all", credentialsInclude)), 1, repo)
|
||||
headers, _ := exec.Command("git", "-C", repo, "config", "--get-all", "http.file://"+server+"/.extraheader").Output()
|
||||
headers, _ := exec.Command("git", "-C", repo, "config", "--get-all", "http."+serverURL+"/.extraheader").Output()
|
||||
assert.Equal(t, persisted, string(headers) == "\nAuthorization: Basic dG9rZW46dG9r\n", repo)
|
||||
}
|
||||
if tc.inputs["ssh-key"] != "" {
|
||||
@@ -354,3 +360,25 @@ func fileExists(name string) bool {
|
||||
_, err := os.Stat(name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func TestWorkspaceDestination(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
windows bool
|
||||
want string
|
||||
}{
|
||||
{`..\x`, true, ""},
|
||||
{`C:x`, true, ""},
|
||||
{`a\b`, true, "/w/a/b"},
|
||||
{`..\x`, false, `/w/..\x`},
|
||||
{`a:b`, false, "/w/a:b"},
|
||||
} {
|
||||
got, err := workspaceDestination("/w", tc.path, tc.windows)
|
||||
if tc.want == "" {
|
||||
assert.ErrorContains(t, err, "must be relative", tc.path)
|
||||
} else {
|
||||
require.NoError(t, err, tc.path)
|
||||
assert.Equal(t, tc.want, got, tc.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user