enhance: add GITEA_* aliases for built-in workflow environment variables (#1358)

Closes https://gitea.com/gitea/runner/issues/1317, which makes https://github.com/go-gitea/gitea/issues/39813 redundant as the runner sets these variables.

Every built-in `GITHUB_*` environment variable the runner sets is now also available as `GITEA_*` with the same value, so Gitea-native workflows don't have to rely on GitHub naming. The `GITEA_*` names mirror the `GITHUB_*` ones after their last write, so they stay equal and are unset together.

Workflow, job and runner `envs` values for these `GITEA_*` names are now overwritten, the same as for `GITHUB_*`.

---------

Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1358
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: KenanZhu <imnanoki@qq.com>
This commit is contained in:
KenanZhu
2026-10-30 21:21:02 +00:00
committed by silverwind
parent d8d4a4657d
commit ca8e9c3c64
10 changed files with 62 additions and 39 deletions

View File

@@ -456,6 +456,7 @@ func evalDockerEnv(ctx context.Context, step step, action *model.Action) (*expre
}
}
mergeIntoMap(step, step.getEnv(), runsEnv, maps.Clone(*step.getEnv()))
aliasGiteaEnv(*step.getEnv())
return ee, nil
}

View File

@@ -19,7 +19,7 @@ import (
)
func TestCompositeActionParity(t *testing.T) {
t.Run("input defaults use the step identity", func(t *testing.T) {
t.Run("input defaults use the step identity and docker env keeps the GITEA aliases", func(t *testing.T) {
for _, uses := range []string{"./action", "own/action@v2"} {
t.Run(uses, func(t *testing.T) {
rc := &RunContext{
@@ -33,13 +33,17 @@ func TestCompositeActionParity(t *testing.T) {
currentStep = &stepActionRemote{RunContext: rc, Step: &model.Step{Uses: uses}, env: map[string]string{}, remoteAction: mustNewRemoteAction(t, uses)}
want = "own/action|v2"
}
action := &model.Action{Inputs: map[string]model.Input{"identity": {Default: "${{ github.action_repository }}|${{ github.action_ref }}"}}}
action := &model.Action{
Inputs: map[string]model.Input{"identity": {Default: "${{ github.action_repository }}|${{ github.action_ref }}"}},
Runs: model.ActionRuns{Env: map[string]string{"GITHUB_WORKFLOW_SHA": "runs-sha"}},
}
require.NoError(t, setupEnv(t.Context(), currentStep))
require.NoError(t, populateEnvsFromInput(t.Context(), currentStep, action))
assert.Equal(t, want, (*currentStep.getEnv())["INPUT_IDENTITY"])
_, err := evalDockerEnv(t.Context(), currentStep, action)
require.NoError(t, err)
assert.Equal(t, want, (*currentStep.getEnv())["identity"])
assert.Equal(t, "runs-sha", (*currentStep.getEnv())["GITEA_WORKFLOW_SHA"])
})
}
})

View File

@@ -84,8 +84,7 @@ func (rc *RunContext) setupHookFileCommands(ctx context.Context, env map[string]
actPath := rc.JobContainer.GetActPath()
env["GITHUB_ENV"] = path.Join(actPath, hookEnvFileCommand)
env["GITHUB_PATH"] = path.Join(actPath, hookPathFileCommand)
env["GITEA_ENV"] = env["GITHUB_ENV"]
env["GITEA_PATH"] = env["GITHUB_PATH"]
aliasGiteaEnv(env)
return rc.JobContainer.Copy(actPath,
&container.FileEntry{Name: hookEnvFileCommand, Mode: 0o666},

View File

@@ -121,6 +121,7 @@ func TestRunJobHook(t *testing.T) {
assert.Equal(t, "job", jobContainer.env["GITHUB_JOB"])
assert.Equal(t, "/var/run/act/workflow/hook-envs.txt", jobContainer.env["GITHUB_ENV"])
assert.Equal(t, "/var/run/act/workflow/hook-path.txt", jobContainer.env["GITHUB_PATH"])
assert.Equal(t, jobContainer.env["GITHUB_PATH"], jobContainer.env["GITEA_PATH"])
})
t.Run("each hook has its own command state", func(t *testing.T) {

View File

@@ -1602,6 +1602,23 @@ func isLocalCheckout(ghc *model.GithubContext, step *model.Step) bool {
return true
}
// aliasGiteaEnv mirrors the runner's GITHUB_* variables as GITEA_*, call it after the last write to them.
func aliasGiteaEnv(env map[string]string) {
for _, suffix := range []string{
"ACTION", "ACTION_PATH", "ACTION_REF", "ACTION_REPOSITORY", "ACTIONS", "ACTOR", "ACTOR_ID", "API_URL", "BASE_REF",
"ENV", "EVENT_NAME", "EVENT_PATH", "GRAPHQL_URL", "HEAD_REF", "JOB", "OUTPUT", "PATH", "REF", "REF_NAME",
"REF_PROTECTED", "REF_TYPE", "REPOSITORY", "REPOSITORY_ID", "REPOSITORY_OWNER", "REPOSITORY_OWNER_ID",
"RETENTION_DAYS", "RUN_ATTEMPT", "RUN_ID", "RUN_NUMBER", "SERVER_URL", "SHA", "STATE", "STEP_SUMMARY",
"TRIGGERING_ACTOR", "WORKFLOW", "WORKFLOW_REF", "WORKFLOW_SHA", "WORKSPACE",
} {
if value, ok := env["GITHUB_"+suffix]; ok {
env["GITEA_"+suffix] = value
} else {
delete(env, "GITEA_"+suffix)
}
}
}
func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubContext, env map[string]string) {
if _, ok := env["CI"]; !ok {
env["CI"] = "true"
@@ -1654,6 +1671,7 @@ func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubCon
env["GITHUB_GRAPHQL_URL"] = ""
env["GITHUB_RUN_ATTEMPT"] = github.RunAttempt
aliasGiteaEnv(env)
env["RUNNER_NAME"] = rc.Config.RunnerName
env["RUNNER_ENVIRONMENT"] = "self-hosted"

View File

@@ -877,25 +877,25 @@ func TestGetGitHubContext(t *testing.T) {
RepositoryID: "34",
RepositoryOwnerID: "56",
WorkflowRef: "owner/repo/.gitea/workflows/test.yml@refs/heads/main",
WorkflowSha: "workflow-sha",
RefProtected: true,
}
ghc = rc.getGithubContext(t.Context())
assert.Equal(t, "preset-actor", ghc.Actor)
assert.Equal(t, "triggerer", ghc.TriggeringActor)
assert.Equal(t, "job1", ghc.Job)
env := map[string]string{}
env := map[string]string{"GITHUB_WORKFLOW_SHA": "workflow-sha"}
rc.withGithubEnv(t.Context(), ghc, env)
for name, value := range map[string]string{
"GITHUB_ACTOR_ID": "12",
"GITHUB_TRIGGERING_ACTOR": "triggerer",
"GITHUB_REPOSITORY_ID": "34",
"GITHUB_REPOSITORY_OWNER_ID": "56",
"GITHUB_WORKFLOW_REF": "owner/repo/.gitea/workflows/test.yml@refs/heads/main",
"GITHUB_WORKFLOW_SHA": "workflow-sha",
"GITHUB_REF_PROTECTED": "true",
for suffix, value := range map[string]string{
"ACTOR_ID": "12",
"TRIGGERING_ACTOR": "triggerer",
"REPOSITORY_ID": "34",
"REPOSITORY_OWNER_ID": "56",
"WORKFLOW_REF": "owner/repo/.gitea/workflows/test.yml@refs/heads/main",
"WORKFLOW_SHA": "workflow-sha",
"REF_PROTECTED": "true",
} {
assert.Equal(t, value, env[name], name)
assert.Equal(t, value, env["GITHUB_"+suffix], suffix)
assert.Equal(t, value, env["GITEA_"+suffix], suffix)
}
ghc.RefProtected = false
rc.withGithubEnv(t.Context(), ghc, env)
@@ -922,12 +922,12 @@ func TestGetGitHubContext(t *testing.T) {
github := rc.getGithubContext(t.Context())
assert.Equal(t, testcase.serverURL, github.ServerURL)
assert.Equal(t, testcase.apiURL, github.APIURL)
env := map[string]string{}
env := map[string]string{"GITEA_SERVER_URL": "stale", "GITEA_API_URL": "stale"}
rc.withGithubEnv(t.Context(), github, env)
_, exported := env["GITHUB_SERVER_URL"]
assert.Equal(t, testcase.serverURL != "", exported)
assert.Equal(t, testcase.serverURL, env["GITHUB_SERVER_URL"])
assert.Equal(t, testcase.apiURL, env["GITHUB_API_URL"])
assert.Equal(t, testcase.serverURL, env["GITEA_SERVER_URL"])
assert.Equal(t, testcase.apiURL, env["GITEA_API_URL"])
})
}
}

View File

@@ -157,15 +157,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
stepSummaryIndex := topRC.CurrentStepIndex
summaryFileCommand := path.Join("workflow", "step-summary-"+strconv.Itoa(stepSummaryIndex)+".md")
(*step.getEnv())["GITHUB_STEP_SUMMARY"] = path.Join(actPath, summaryFileCommand)
{
// For Gitea
(*step.getEnv())["GITEA_OUTPUT"] = (*step.getEnv())["GITHUB_OUTPUT"]
(*step.getEnv())["GITEA_STATE"] = (*step.getEnv())["GITHUB_STATE"]
(*step.getEnv())["GITEA_PATH"] = (*step.getEnv())["GITHUB_PATH"]
(*step.getEnv())["GITEA_ENV"] = (*step.getEnv())["GITHUB_ENV"]
(*step.getEnv())["GITEA_STEP_SUMMARY"] = (*step.getEnv())["GITHUB_STEP_SUMMARY"]
}
aliasGiteaEnv(*step.getEnv())
// Reset the per-phase file-command files. GITHUB_STEP_SUMMARY is intentionally
// excluded here and initialized below at most once per step so writes from later

View File

@@ -58,7 +58,10 @@ func TestStepRun(t *testing.T) {
},
}
sr.RunContext.ExprEval = sr.RunContext.NewExpressionEvaluator(t.Context())
require.NoError(t, sr.Step.Env.Encode(map[string]string{"GITHUB_ACTION_REPOSITORY": "forged/repo", "GITHUB_ACTION_REF": "forged-ref"}))
require.NoError(t, sr.Step.Env.Encode(map[string]string{
"GITHUB_ACTION_REPOSITORY": "forged/repo", "GITHUB_ACTION_REF": "forged-ref",
"GITEA_ACTION_REPOSITORY": "forged/repo", "GITEA_ACTION_REF": "forged-ref",
}))
cm.On("Copy", "/var/run/act", []*container.FileEntry{fileEntry}).Return(noopExecutor)
cm.On("Exec", []string{"bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "/var/run/act/workflow/1.sh"}, mock.AnythingOfType("map[string]string"), "", "workdir").Return(noopExecutor)
@@ -67,6 +70,9 @@ func TestStepRun(t *testing.T) {
require.NoError(t, sr.main()(t.Context()))
assert.Empty(t, sr.env["GITHUB_ACTION_REPOSITORY"])
assert.Empty(t, sr.env["GITHUB_ACTION_REF"])
assert.Empty(t, sr.env["GITEA_ACTION_REPOSITORY"])
assert.Empty(t, sr.env["GITEA_ACTION_REF"])
assert.Equal(t, sr.env["GITHUB_STEP_SUMMARY"], sr.env["GITEA_STEP_SUMMARY"])
assert.Equal(t, "other/composite", sr.RunContext.Env["GITHUB_ACTION_REPOSITORY"])
assert.Equal(t, "v1", sr.RunContext.Env["GITHUB_ACTION_REF"])

View File

@@ -155,18 +155,14 @@ func TestSetupEnv(t *testing.T) {
require.NoError(t, setupEnv(t.Context(), testStep))
require.NoError(t, setupInputs(t.Context(), testStep))
delete(env, "GITHUB_REF")
delete(env, "GITHUB_REF_NAME")
delete(env, "GITHUB_REF_TYPE")
delete(env, "GITHUB_SHA")
delete(env, "GITHUB_WORKSPACE")
delete(env, "GITHUB_REPOSITORY")
delete(env, "GITHUB_REPOSITORY_OWNER")
delete(env, "GITHUB_ACTOR")
for _, suffix := range []string{"REF", "REF_NAME", "REF_TYPE", "SHA", "WORKSPACE", "REPOSITORY", "REPOSITORY_OWNER", "ACTOR"} {
delete(env, "GITHUB_"+suffix)
delete(env, "GITEA_"+suffix)
}
delete(env, "RUNNER_NAME")
delete(env, "RUNNER_WORKSPACE")
assert.Equal(t, map[string]string{
expected := map[string]string{
"ACT": "true",
"ACT_SKIP_CHECKOUT": "true",
"CI": "true",
@@ -193,7 +189,13 @@ func TestSetupEnv(t *testing.T) {
"RUNNER_ENVIRONMENT": "self-hosted",
"RUNNER_PERFLOG": "/dev/null",
"RUNNER_TRACKING_ID": "",
}, env)
}
for name, value := range expected {
if suffix, ok := strings.CutPrefix(name, "GITHUB_"); ok {
expected["GITEA_"+suffix] = value
}
}
assert.Equal(t, expected, env)
for _, expression := range []string{"inputs.args", "env.ARGS"} {
t.Run("deferred inputs from "+expression, func(t *testing.T) {

View File

@@ -45,7 +45,7 @@ A hook path that does not exist inside the job environment fails the job with `N
## Environment
A hook sees the job's environment: the workflow, job and `container:` `env:`, the runner's `envs`, and the `GITHUB_*` context variables, with the same masking applied to its output as to a step's. The step-specific ones (`GITHUB_ACTION`, `GITHUB_OUTPUT`, `GITHUB_STATE`) are not set — a hook is not a step, so `::save-state::` and `::set-output::` have nowhere to go.
A hook sees the job's environment: the workflow, job and `container:` `env:`, the runner's `envs`, and the `GITHUB_*`/`GITEA_*` context variables, with the same masking applied to its output as to a step's. The step-specific ones (`GITHUB_ACTION`, `GITHUB_OUTPUT`, `GITHUB_STATE`) are not set — a hook is not a step, so `::save-state::` and `::set-output::` have nowhere to go.
Its stdout is part of the job log, after a collapsible group naming the hook and its shell, and is scanned for workflow commands. `::add-mask::` registers a value to be masked for the rest of the job. `::set-env::` and `::add-path::` apply to the steps that follow, but as in a step they need `ACTIONS_ALLOW_UNSECURE_COMMANDS: true`, and a refused one fails the hook.