mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-12 19:03:45 +00:00
fix: align common behaviour with GitHub's runner (#1413)
Closes differences from GitHub's runner that common workflows hit:
- `ACTIONS_STEP_DEBUG` set as a repository variable enables debug output as on GitHub, where a secret of the same name still takes precedence. `runner.debug` and `RUNNER_DEBUG` are now also set inside composite actions, which lost the secret.
- `github.env` and `github.path` hold the file-command paths instead of being empty, so `echo "A=1" >> "${{ github.env }}"` works.
- Remote composite actions only get a pre stage when they nest actions. That stage evaluated `with:` before earlier steps ran, so an input like `${{ fromJSON(steps.meta.outputs.json).v }}` failed the job.
- `ports` on a job container are published, they were ignored while service ports worked.
- Dockerfile action build output reaches the job log. It was logged at debug and dropped, so a failing `RUN` showed only the final error.
- Local Docker actions honor their `.dockerignore`, as `docker build` does.
- Cancelling or timing out a host-mode step sends SIGINT, then SIGTERM after 7.5 seconds and SIGKILL after 2.5 more, as GitHub's runner does, so `trap` handlers and shutdown hooks run.
- `.sh` job hooks fall back to `sh` where bash is missing, and `.ps1` hooks to `powershell` where pwsh is missing, as on GitHub.
_Written by Claude (Opus 5.5)._
Reviewed-on: https://gitea.com/gitea/runner/pulls/1413
Reviewed-by: bircni <[email protected]>
Co-authored-by: silverwind <[email protected]>
This commit is contained in:
1 parent
0dfec377d2
commit
645b17de72
25 files changed
+340
-91
No files matched your search
@@ -26,4 +26,6 @@ func (k *Killer) Kill() error {
|
||||
return k.p.Kill()
|
||||
}
|
||||
|
||||
func (k *Killer) cancel(_process *os.Process, _graceful bool) error { return k.Kill() }
|
||||
|
||||
func (k *Killer) Close() error { return nil }
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Killer terminates a started process together with its whole process group,
|
||||
@@ -52,5 +53,37 @@ func (k *Killer) Kill() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Killer) cancel(process *os.Process, graceful bool) error {
|
||||
if graceful && k.pgid > 0 {
|
||||
for _, stage := range []struct {
|
||||
signal syscall.Signal
|
||||
delay time.Duration
|
||||
}{
|
||||
{syscall.SIGINT, 7500 * time.Millisecond},
|
||||
{syscall.SIGTERM, 2500 * time.Millisecond},
|
||||
} {
|
||||
if process.Signal(syscall.Signal(0)) != nil || syscall.Kill(-k.pgid, stage.signal) != nil || waitForExit(process, stage.delay) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return k.Kill()
|
||||
}
|
||||
|
||||
func waitForExit(process *os.Process, delay time.Duration) bool {
|
||||
timer := time.NewTimer(delay)
|
||||
defer timer.Stop()
|
||||
ticker := time.NewTicker(10 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for !errors.Is(process.Signal(syscall.Signal(0)), os.ErrProcessDone) {
|
||||
select {
|
||||
case <-timer.C:
|
||||
return false
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Close is a no-op on Unix; there is no job handle to release.
|
||||
func (k *Killer) Close() error { return nil }
|
||||
@@ -6,9 +6,11 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
@@ -95,3 +97,34 @@ func TestTreeKillCancelBeforeCaptureKillsTree(t *testing.T) {
|
||||
require.Error(t, cmd.Wait())
|
||||
require.Eventually(t, func() bool { return !processAlive(childPID) }, 5*time.Second, 10*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestTreeKillCancelRunsInterruptTrap(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
marker := filepath.Join(t.TempDir(), "interrupted")
|
||||
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", `trap 'echo interrupted > "$1"; exit 0' INT; echo ready; read value`, "sh", marker)
|
||||
cmd.SysProcAttr = SysProcAttr("", false)
|
||||
input, err := cmd.StdinPipe()
|
||||
require.NoError(t, err)
|
||||
defer input.Close()
|
||||
output, err := cmd.StdoutPipe()
|
||||
require.NoError(t, err)
|
||||
treeKill := NewTreeKill(cmd)
|
||||
treeKill.GracefulCancel = true
|
||||
require.NoError(t, cmd.Start())
|
||||
killer, err := treeKill.Capture(cmd.Process)
|
||||
require.NoError(t, err)
|
||||
defer killer.Close()
|
||||
t.Cleanup(func() {
|
||||
_ = killer.Kill()
|
||||
_ = cmd.Wait()
|
||||
})
|
||||
var ready string
|
||||
_, err = fmt.Fscanln(output, &ready)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "ready", ready)
|
||||
|
||||
cancel()
|
||||
require.ErrorIs(t, treeKill.Wait(ctx, cmd), context.Canceled)
|
||||
require.FileExists(t, marker)
|
||||
}
|
||||
@@ -68,6 +68,8 @@ func (k *Killer) Kill() error {
|
||||
return windows.TerminateJobObject(k.job, 1)
|
||||
}
|
||||
|
||||
func (k *Killer) cancel(_process *os.Process, _graceful bool) error { return k.Kill() }
|
||||
|
||||
// Close releases the job handle. It does not terminate the processes.
|
||||
func (k *Killer) Close() error {
|
||||
if k == nil {
|
||||
|
||||
@@ -32,8 +32,9 @@ const treeKillWaitDelay = 10 * time.Second
|
||||
// Callers still set cmd.SysProcAttr (via SysProcAttr) themselves, because the
|
||||
// value differs between the plain and PTY execution paths.
|
||||
type TreeKill struct {
|
||||
killer atomic.Pointer[Killer]
|
||||
KeepLeftovers bool
|
||||
killer atomic.Pointer[Killer]
|
||||
KeepLeftovers bool
|
||||
GracefulCancel bool
|
||||
}
|
||||
|
||||
// NewTreeKill sets cmd.Cancel, cmd.WaitDelay and CREATE_SUSPENDED. Call it after setting cmd.SysProcAttr, and Capture once after a successful Start.
|
||||
@@ -42,12 +43,12 @@ func NewTreeKill(cmd *exec.Cmd) *TreeKill {
|
||||
t := &TreeKill{}
|
||||
cmd.Cancel = func() error {
|
||||
if k := t.killer.Load(); k != nil {
|
||||
return k.Kill()
|
||||
return k.cancel(cmd.Process, t.GracefulCancel)
|
||||
}
|
||||
if cmd.Process != nil {
|
||||
if killer, err := NewKiller(cmd.Process); err == nil {
|
||||
defer killer.Close()
|
||||
return killer.Kill()
|
||||
return killer.cancel(cmd.Process, t.GracefulCancel)
|
||||
}
|
||||
return cmd.Process.Kill()
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ func TestTreeKillWaitWithLeftoverHoldingOutput(t *testing.T) {
|
||||
cmd.Stdout = io.Discard
|
||||
cmd.SysProcAttr = SysProcAttr("", false)
|
||||
tk := NewTreeKill(cmd)
|
||||
tk.GracefulCancel = true
|
||||
cmd.WaitDelay = 50 * time.Millisecond
|
||||
require.NoError(t, cmd.Start())
|
||||
killer, err := tk.Capture(cmd.Process)
|
||||
|
||||
Reference in new issue
Block a user