mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-12 19:03:45 +00:00
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]>
131 lines
3.9 KiB
Go
131 lines
3.9 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//go:build !windows && !plan9
|
|
|
|
package process
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// processAlive reports whether pid refers to a still-running process. Signal 0
|
|
// performs error checking without delivering a signal: a nil error (or EPERM)
|
|
// means the process exists, ESRCH means it is gone.
|
|
//
|
|
// On Linux, zombie processes (state Z in /proc/<pid>/stat) appear alive to
|
|
// kill(0) but have already terminated — their corpse lingers until the parent
|
|
// calls wait(). In a Docker container the child may be reparented to a PID 1
|
|
// that does not reap promptly, so we treat zombies as not alive.
|
|
func processAlive(pid int) bool {
|
|
err := syscall.Kill(pid, 0)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
// On Linux /proc is available; check whether the process is a zombie.
|
|
if b, readErr := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)); readErr == nil {
|
|
// Format: "pid (comm) state ..." — state follows the closing ')' of the
|
|
// command name (which may itself contain spaces and parens).
|
|
if _, after, found := strings.CutLast(string(b), ") "); found {
|
|
fields := strings.Fields(after)
|
|
if len(fields) > 0 && fields[0] == "Z" {
|
|
return false // zombie: terminated but not yet reaped
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestKillerKillsTree(t *testing.T) {
|
|
cmd, ready := newTestProcess(t, "parent", false)
|
|
require.NoError(t, cmd.Start())
|
|
t.Cleanup(func() {
|
|
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
|
_ = cmd.Wait()
|
|
})
|
|
|
|
killer, err := NewKiller(cmd.Process)
|
|
require.NoError(t, err)
|
|
defer killer.Close()
|
|
|
|
childPID := ready()
|
|
require.True(t, processAlive(childPID), "child process should start")
|
|
|
|
require.NoError(t, killer.Kill())
|
|
_ = cmd.Wait()
|
|
|
|
require.Eventually(t, func() bool {
|
|
return !processAlive(childPID)
|
|
}, 20*time.Second, 100*time.Millisecond, "backgrounded child should be terminated")
|
|
}
|
|
|
|
func TestTreeKillCancelBeforeCaptureKillsTree(t *testing.T) {
|
|
stdin, input, err := os.Pipe()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = stdin.Close() })
|
|
t.Cleanup(func() { _ = input.Close() })
|
|
cmd := exec.CommandContext(t.Context(), "/bin/sh", "-c", "exec 3<&0; cat <&3 & echo $!; read value")
|
|
cmd.Stdin = stdin
|
|
cmd.SysProcAttr = SysProcAttr("", false)
|
|
output, err := cmd.StdoutPipe()
|
|
require.NoError(t, err)
|
|
treeKill := NewTreeKill(cmd)
|
|
require.NoError(t, cmd.Start())
|
|
t.Cleanup(func() {
|
|
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
|
_ = cmd.Wait()
|
|
})
|
|
var childPID int
|
|
_, err = fmt.Fscan(output, &childPID)
|
|
require.NoError(t, err)
|
|
require.True(t, processAlive(childPID))
|
|
|
|
require.NoError(t, cmd.Cancel())
|
|
killer, err := treeKill.Capture(cmd.Process)
|
|
require.NoError(t, err)
|
|
defer killer.Close()
|
|
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)
|
|
}
|