mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-04 07:33:47 +00:00
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 <bircni@icloud.com>
137 lines
4.9 KiB
Go
137 lines
4.9 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package run
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.com/gitea/runner/act/common"
|
|
"gitea.com/gitea/runner/act/common/logging"
|
|
"gitea.com/gitea/runner/internal/pkg/config"
|
|
"gitea.com/gitea/runner/internal/pkg/metrics"
|
|
"gitea.com/gitea/runner/internal/pkg/process"
|
|
"gitea.com/gitea/runner/internal/pkg/report"
|
|
|
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
|
)
|
|
|
|
func (r *Runner) runPostTaskScript(ctx context.Context, reporter *report.Reporter, task *runnerv1.Task, workdir string) {
|
|
script := r.cfg.Runner.PostTaskScript
|
|
if script == "" {
|
|
return
|
|
}
|
|
|
|
timeout := r.cfg.Runner.PostTaskScriptTimeout
|
|
if timeout <= 0 {
|
|
timeout = config.DefaultPostTaskScriptTimeout
|
|
}
|
|
|
|
scriptCtx, cancel := postTaskScriptContext(ctx, timeout)
|
|
defer cancel()
|
|
|
|
env := r.postTaskScriptEnv(reporter, task, workdir)
|
|
logging.Infof("running post-task script %q for task %d", script, task.Id)
|
|
|
|
cmd := scriptCommand(scriptCtx, script)
|
|
cmd.Env = envListFromMap(env)
|
|
|
|
stdout := postTaskScriptLogWriter("stdout")
|
|
stderr := postTaskScriptLogWriter("stderr")
|
|
cmd.Stdout = stdout
|
|
cmd.Stderr = stderr
|
|
|
|
// Kill the script's whole process tree on cancellation and bound the post-exit
|
|
// I/O wait, so a backgrounded child inheriting cmd's stdout/stderr pipe can
|
|
// never hang cmd.Wait() and the runner. See process.TreeKill.
|
|
treeKill := process.NewTreeKill(cmd)
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
logging.Warnf("post-task script %q for task %d: %v", script, task.Id, err)
|
|
return
|
|
}
|
|
if k, kerr := treeKill.Capture(cmd.Process); kerr != nil {
|
|
logging.Warnf("post-task script %q for task %d: process tree kill setup failed, falling back to single-process kill: %v", script, task.Id, kerr)
|
|
} else {
|
|
defer k.Close()
|
|
}
|
|
|
|
err := cmd.Wait()
|
|
// Flush any trailing, not-yet-newline-terminated output now that the I/O
|
|
// copiers have finished (cmd.Wait, bounded by WaitDelay above, guarantees it).
|
|
common.FlushWriter(stdout)
|
|
common.FlushWriter(stderr)
|
|
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok {
|
|
logging.Warnf("post-task script %q for task %d exited with code %d", script, task.Id, exitErr.ExitCode())
|
|
} else if err != nil {
|
|
logging.Warnf("post-task script %q for task %d: %v", script, task.Id, err)
|
|
}
|
|
}
|
|
|
|
func scriptCommand(ctx context.Context, script string) *exec.Cmd { // Windows batch files need cmd.exe, CreateProcess cannot start them
|
|
if ext := strings.ToLower(filepath.Ext(script)); runtime.GOOS == "windows" && (ext == ".bat" || ext == ".cmd") {
|
|
cmd := exec.CommandContext(ctx, "cmd.exe")
|
|
cmd.SysProcAttr = process.SysProcAttr(`cmd.exe /d /s /c ""`+script+`""`, false)
|
|
return cmd
|
|
}
|
|
cmd := exec.CommandContext(ctx, script)
|
|
cmd.SysProcAttr = process.SysProcAttr("", false)
|
|
return cmd
|
|
}
|
|
|
|
func postTaskScriptContext(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
|
|
// Detach from the task context's deadline and cancellation: the task has
|
|
// already finished by the time the post-task script runs, so the script must
|
|
// get its full configured timeout. Inheriting the task deadline would silently
|
|
// truncate that budget when the job completed close to its own timeout (and an
|
|
// already-cancelled task context would skip the script entirely).
|
|
// context.WithoutCancel keeps the context values while dropping the deadline.
|
|
return context.WithTimeout(context.WithoutCancel(ctx), timeout)
|
|
}
|
|
|
|
func (r *Runner) postTaskScriptEnv(reporter *report.Reporter, task *runnerv1.Task, workdir string) map[string]string {
|
|
env := r.cloneEnvs()
|
|
env["GITEA_TASK_ID"] = strconv.FormatInt(task.Id, 10)
|
|
env["GITEA_WORKSPACE"] = workdir
|
|
// GITEA_JOB_RESULT shares the runner's canonical result vocabulary
|
|
// (success/failure/cancelled/skipped/unknown), the same strings the reporter
|
|
// parses and the metrics labels use.
|
|
env["GITEA_JOB_RESULT"] = metrics.ResultToStatusLabel(reporter.Result())
|
|
if v := task.Context.Fields["run_id"].GetStringValue(); v != "" {
|
|
env["GITEA_RUN_ID"] = v
|
|
}
|
|
if v := task.Context.Fields["repository"].GetStringValue(); v != "" {
|
|
env["GITEA_REPOSITORY"] = v
|
|
}
|
|
return env
|
|
}
|
|
|
|
func envListFromMap(env map[string]string) []string {
|
|
envList := make([]string, 0, len(env))
|
|
for key, value := range env {
|
|
envList = append(envList, key+"="+value)
|
|
}
|
|
return envList
|
|
}
|
|
|
|
// postTaskScriptLogWriter returns an io.Writer that logs the script's output one
|
|
// line at a time, tagged with the stream name. It is passed as cmd.Stdout/Stderr
|
|
// (rather than a StdoutPipe) so that cmd.WaitDelay governs the copying goroutine:
|
|
// a backgrounded process holding the pipe open can never block cmd.Wait()
|
|
// indefinitely. Flush any trailing partial line with common.FlushWriter after
|
|
// cmd.Wait() returns.
|
|
func postTaskScriptLogWriter(stream string) io.Writer {
|
|
return common.NewLineWriter(func(line string) bool {
|
|
logging.Infof("post-task script %s: %s", stream, strings.TrimRight(line, "\r\n"))
|
|
return true
|
|
})
|
|
}
|