feat: report runner name, environment, workspace and debug to jobs (#1105)

Passes the `runner` context values act already knew but never reported to jobs:

1. `runner.name` / `RUNNER_NAME` — registered runner name, hostname for `exec`
2. `runner.environment` / `RUNNER_ENVIRONMENT` — `self-hosted`
3. `RUNNER_WORKSPACE` — parent of `GITHUB_WORKSPACE`
4. `runner.debug` / `RUNNER_DEBUG` — `1` when `ACTIONS_STEP_DEBUG` is set

`ImageOS` now prefers the release named in the resolved image tag, so `ubuntu-latest` mapped to `runner-images:ubuntu-24.04` reports `ubuntu24` instead of the hardcoded `ubuntu20`. The `runs-on` label stays the fallback.

---------

Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1105
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni
2026-07-28 05:29:34 +00:00
committed by silverwind
parent c3b39e0d99
commit 333eb17d19
6 changed files with 210 additions and 19 deletions

View File

@@ -95,9 +95,7 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
Inputs: inputs,
HashFiles: getHashFilesFunction(ctx, rc),
}
if rc.JobContainer != nil {
ee.Runner = rc.JobContainer.GetRunnerContext(ctx)
}
ee.Runner = rc.getRunnerContext(ctx)
return expressionEvaluator{
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
Run: rc.Run,
@@ -149,9 +147,7 @@ func (rc *RunContext) NewStepExpressionEvaluator(ctx context.Context, step step)
Inputs: inputs,
HashFiles: getHashFilesFunction(ctx, rc),
}
if rc.JobContainer != nil {
ee.Runner = rc.JobContainer.GetRunnerContext(ctx)
}
ee.Runner = rc.getRunnerContext(ctx)
return expressionEvaluator{
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
Run: rc.Run,

View File

@@ -298,7 +298,7 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
AllocatePTY: rc.Config.AllocatePTY,
}
rc.cleanUpJobContainer = rc.JobContainer.Remove()
for k, v := range rc.JobContainer.GetRunnerContext(ctx) {
for k, v := range rc.getRunnerContext(ctx) {
if v, ok := v.(string); ok {
rc.Env["RUNNER_"+strings.ToUpper(k)] = v
}
@@ -986,6 +986,21 @@ func (rc *RunContext) getStepsContext() map[string]*model.StepResult {
return rc.StepResults
}
// getRunnerContext returns the `runner` context: what the execution environment knows
// (os, arch, temp, tool_cache) plus what only the runner process knows.
func (rc *RunContext) getRunnerContext(ctx context.Context) map[string]any {
runnerContext := map[string]any{}
if rc.JobContainer != nil {
maps0.Copy(runnerContext, rc.JobContainer.GetRunnerContext(ctx))
}
runnerContext["name"] = rc.Config.RunnerName
runnerContext["environment"] = "self-hosted"
if rc.Config.RunnerDebug() {
runnerContext["debug"] = "1"
}
return runnerContext
}
func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext {
logger := common.Logger(ctx)
ghc := &model.GithubContext{
@@ -1170,7 +1185,7 @@ func nestedMapLookup(m map[string]any, ks ...string) (rval any) {
}
}
func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubContext, env map[string]string) map[string]string { //nolint:unparam // pre-existing issue from nektos/act
func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubContext, env map[string]string) {
env["CI"] = "true"
env["GITHUB_WORKFLOW"] = github.Workflow
env["GITHUB_RUN_ID"] = github.RunID
@@ -1212,23 +1227,71 @@ func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubCon
env["GITHUB_RUN_ATTEMPT"] = github.RunAttempt
}
env["RUNNER_NAME"] = rc.Config.RunnerName
env["RUNNER_ENVIRONMENT"] = "self-hosted"
if workspace := parentDir(github.Workspace); workspace != "" {
env["RUNNER_WORKSPACE"] = workspace
}
if rc.Config.RunnerDebug() {
env["RUNNER_DEBUG"] = "1"
}
if rc.Config.ArtifactServerPath != "" {
setActionRuntimeVars(rc, env)
}
for _, platformName := range rc.runsOnPlatformNames(ctx) {
if platformName != "" {
if platformName == "ubuntu-latest" {
// hardcode current ubuntu-latest since we have no way to check that 'on the fly'
env["ImageOS"] = "ubuntu20"
} else {
platformName = strings.SplitN(strings.Replace(platformName, `-`, ``, 1), `.`, 2)[0]
env["ImageOS"] = platformName
}
}
if imageOS := rc.imageOS(ctx); imageOS != "" {
env["ImageOS"] = imageOS
}
}
// parentDir returns the directory containing p, or "" when p names no parent. Both
// separators are accepted rather than filepath's, as p may describe a container while
// the runner itself runs on Windows, or the other way round.
func parentDir(p string) string {
if slash := strings.LastIndexAny(p, `/\`); slash > 0 {
return p[:slash]
}
return ""
}
// imageOS returns ImageOS, which setup-* actions use to tell one runner image release
// from another. The resolved image tag is preferred over the runs-on label because it
// still names a release when the label is a rolling one such as ubuntu-latest.
func (rc *RunContext) imageOS(ctx context.Context) string {
if rc.Run.Job().RunsOn() == nil {
// A composite action runs on a synthetic job, and resolving its image would only
// log that runs-on is missing.
return ""
}
if imageOS := imageOSFromImage(rc.platformImage(ctx)); imageOS != "" {
return imageOS
}
return env
for _, platformName := range slices.Backward(rc.runsOnPlatformNames(ctx)) {
if platformName == "ubuntu-latest" {
// Rolling label whose image names no release either, so keep the historical value.
return "ubuntu20"
} else if platformName != "" {
return strings.SplitN(strings.Replace(platformName, `-`, ``, 1), `.`, 2)[0]
}
}
return ""
}
// imageOSTag matches an image reference tagged with an OS family ImageOS can report plus
// its release, such as "docker.gitea.com/runner-images:ubuntu-24.04". Anything else
// ("ubuntu-latest", "app:22.04", "catthehacker/ubuntu:act-22.04", or a registry port) is
// left to the runs-on label rather than turned into a bogus OS.
var imageOSTag = regexp.MustCompile(`:(ubuntu|win|macos)-?([0-9]+)[^/]*$`)
// imageOSFromImage derives ImageOS from an image reference, e.g.
// "docker.gitea.com/runner-images:ubuntu-24.04" yields "ubuntu24".
func imageOSFromImage(image string) string {
if match := imageOSTag.FindStringSubmatch(image); match != nil {
return match[1] + match[2]
}
return ""
}
func setActionRuntimeVars(rc *RunContext, env map[string]string) {

View File

@@ -940,3 +940,117 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
assert.NotPanics(t, func() { (&RunContext{}).cleanupFailedStart(context.Background()) })
})
}
func TestImageOSFromImage(t *testing.T) {
for _, tc := range []struct {
image string
want string
}{
{"", ""},
{"docker.gitea.com/runner-images:ubuntu-24.04", "ubuntu24"},
{"docker.gitea.com/runner-images:ubuntu-latest", ""},
{"runner-images:ubuntu22.04", "ubuntu22"},
{"node:20", ""},
{"ubuntu:22.04", ""},
{"ubuntu", ""},
{"catthehacker/ubuntu:act-22.04", ""},
{"myco/ubuntu:v2.1", ""},
{"myco/ubuntu:v22.04", ""},
{"app:release-1", ""},
{"app:1.2.3", ""},
{"app:build-2.1", ""},
{"registry.example.com:5000/runner-images", ""},
{"registry.example.com:5000/runner-images:ubuntu-24.04", "ubuntu24"},
} {
t.Run(tc.image, func(t *testing.T) {
assert.Equal(t, tc.want, imageOSFromImage(tc.image))
})
}
}
func createRunsOnRunContext(t *testing.T, runsOn string) *RunContext {
return createIfTestRunContext(map[string]*model.Job{
"job1": createJob(t, "runs-on: "+runsOn, ""),
})
}
func TestRunContextImageOS(t *testing.T) {
ctx := context.Background()
t.Run("prefers the release in the resolved image tag", func(t *testing.T) {
rc := createRunsOnRunContext(t, "ubuntu-latest")
rc.Config.Platforms = map[string]string{
"ubuntu-latest": "docker.gitea.com/runner-images:ubuntu-24.04",
}
assert.Equal(t, "ubuntu24", rc.imageOS(ctx))
})
t.Run("falls back to the runs-on label", func(t *testing.T) {
rc := createRunsOnRunContext(t, "ubuntu-22.04")
rc.Config.Platforms = map[string]string{"ubuntu-22.04": "some-image"}
assert.Equal(t, "ubuntu22", rc.imageOS(ctx))
})
t.Run("keeps the historical value for a rolling label with no release", func(t *testing.T) {
assert.Equal(t, "ubuntu20", createRunsOnRunContext(t, "ubuntu-latest").imageOS(ctx))
})
t.Run("is empty for the synthetic job of a composite action", func(t *testing.T) {
rc := createIfTestRunContext(map[string]*model.Job{"job1": {}})
assert.Empty(t, rc.imageOS(ctx))
})
}
func TestRunContextGetRunnerContext(t *testing.T) {
ctx := context.Background()
t.Run("adds the runner values the container cannot know", func(t *testing.T) {
rc := createRunsOnRunContext(t, "ubuntu-latest")
rc.Config.RunnerName = "runner-1"
runnerContext := rc.getRunnerContext(ctx)
assert.Equal(t, "runner-1", runnerContext["name"])
assert.Equal(t, "self-hosted", runnerContext["environment"])
assert.NotContains(t, runnerContext, "debug")
})
t.Run("reports debug when step debugging is on", func(t *testing.T) {
rc := createRunsOnRunContext(t, "ubuntu-latest")
rc.Config.Secrets = map[string]string{"ACTIONS_STEP_DEBUG": "true"}
assert.Equal(t, "1", rc.getRunnerContext(ctx)["debug"])
})
t.Run("keeps the execution environment values", func(t *testing.T) {
rc := createRunsOnRunContext(t, "ubuntu-latest")
rc.JobContainer = &container.HostEnvironment{TmpDir: "/tmp/act", ToolCache: "/tmp/tool_cache"}
runnerContext := rc.getRunnerContext(ctx)
assert.Equal(t, "/tmp/act", runnerContext["temp"])
assert.Equal(t, "/tmp/tool_cache", runnerContext["tool_cache"])
assert.NotEmpty(t, runnerContext["os"])
})
}
func TestParentDir(t *testing.T) {
assert.Empty(t, parentDir(""))
assert.Empty(t, parentDir("repo"))
assert.Empty(t, parentDir("/repo"))
assert.Equal(t, "/workspace/owner", parentDir("/workspace/owner/repo"))
assert.Equal(t, `C:\workspace\owner`, parentDir(`C:\workspace\owner\repo`))
}
func TestRunContextWithGithubEnvRunnerValues(t *testing.T) {
ctx := context.Background()
rc := createRunsOnRunContext(t, "ubuntu-latest")
rc.Config.RunnerName = "runner-1"
rc.Config.Secrets = map[string]string{"ACTIONS_STEP_DEBUG": "true"}
env := map[string]string{}
rc.withGithubEnv(ctx, &model.GithubContext{Workspace: "/workspace/owner/repo"}, env)
assert.Equal(t, "runner-1", env["RUNNER_NAME"])
assert.Equal(t, "self-hosted", env["RUNNER_ENVIRONMENT"])
assert.Equal(t, "/workspace/owner", env["RUNNER_WORKSPACE"])
assert.Equal(t, "1", env["RUNNER_DEBUG"])
}

View File

@@ -92,6 +92,14 @@ type Config struct {
InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance
MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count)
AllocatePTY bool // allocate a pseudo-TTY for each step's process
RunnerName string // name this runner registered with, reported as `runner.name`, defaults to the hostname
}
// RunnerDebug reports whether debug logging is on, exposed as `runner.debug` and
// RUNNER_DEBUG. Only the secret also makes the reporter keep ::debug:: output, the env
// is accepted for `exec` and for runners configured with it.
func (c Config) RunnerDebug() bool {
return c.Secrets["ACTIONS_STEP_DEBUG"] == "true" || c.Env["ACTIONS_STEP_DEBUG"] == "true"
}
// GetToken: Adapt to Gitea
@@ -137,6 +145,11 @@ func New(runnerConfig *Config) (Runner, error) {
}
func (runner *runnerImpl) configure() (Runner, error) {
if runner.config.RunnerName == "" {
// Callers that do not register, such as `exec`, still get a `runner.name`.
runner.config.RunnerName, _ = os.Hostname()
}
runner.eventJSON = "{}"
if runner.config.EventJSON != "" {
runner.eventJSON = runner.config.EventJSON

View File

@@ -167,6 +167,9 @@ func TestSetupEnv(t *testing.T) {
delete((env), "GITHUB_REPOSITORY")
delete((env), "GITHUB_REPOSITORY_OWNER")
delete((env), "GITHUB_ACTOR")
// Host-dependent, asserted in TestRunContextWithGithubEnvRunnerValues instead.
delete((env), "RUNNER_NAME")
delete((env), "RUNNER_WORKSPACE")
assert.Equal(t, map[string]string{
"ACT": "true",
@@ -192,6 +195,7 @@ func TestSetupEnv(t *testing.T) {
"GITHUB_WORKFLOW": "",
"INPUT_STEP_WITH": "with-value",
"RC_KEY": "rcvalue",
"RUNNER_ENVIRONMENT": "self-hosted",
"RUNNER_PERFLOG": "/dev/null",
"RUNNER_TRACKING_ID": "",
}, env)

View File

@@ -474,6 +474,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
Vars: task.Vars,
ValidVolumes: r.cfg.Container.ValidVolumes,
InsecureSkipTLS: r.cfg.Runner.Insecure,
RunnerName: r.name,
}
rr, err := runner.New(runnerConfig)