mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-05-01 00:10:31 +08:00
Compare commits
5 Commits
v0.5.0
...
fix/job-en
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e23fda0aca | ||
|
|
66a723f9a6 | ||
|
|
547a0ff297 | ||
|
|
f2b4dbf05f | ||
|
|
bad4239d18 |
@@ -30,6 +30,11 @@ const (
|
|||||||
gray = 37
|
gray = 37
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rawOutputField = "raw_output"
|
||||||
|
scriptLineCyanField = "script_line_cyan"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
colors []int
|
colors []int
|
||||||
nextColor int
|
nextColor int
|
||||||
@@ -161,6 +166,8 @@ func withStepLogger(ctx context.Context, stepNumber int, stepID, stepName, stage
|
|||||||
|
|
||||||
type entryProcessor func(entry *logrus.Entry) *logrus.Entry
|
type entryProcessor func(entry *logrus.Entry) *logrus.Entry
|
||||||
|
|
||||||
|
// valueMasker applies secrets and ::add-mask:: patterns to every log entry, including
|
||||||
|
// raw_output (command/stream) lines; there is no bypass by field.
|
||||||
func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor {
|
func valueMasker(insecureSecrets bool, secrets map[string]string) entryProcessor {
|
||||||
return func(entry *logrus.Entry) *logrus.Entry {
|
return func(entry *logrus.Entry) *logrus.Entry {
|
||||||
if insecureSecrets {
|
if insecureSecrets {
|
||||||
@@ -227,8 +234,12 @@ func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
|
|||||||
debugFlag = "[DEBUG] "
|
debugFlag = "[DEBUG] "
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry.Data["raw_output"] == true {
|
if entry.Data[rawOutputField] == true {
|
||||||
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m %s", f.color, entry.Message)
|
if entry.Data[scriptLineCyanField] == true {
|
||||||
|
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m \x1b[36;1m%s\x1b[0m", f.color, entry.Message)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m %s", f.color, entry.Message)
|
||||||
|
}
|
||||||
} else if entry.Data["dryrun"] == true {
|
} else if entry.Data["dryrun"] == true {
|
||||||
fmt.Fprintf(b, "\x1b[1m\x1b[%dm\x1b[7m*DRYRUN*\x1b[0m \x1b[%dm[%s] \x1b[0m%s%s", gray, f.color, job, debugFlag, entry.Message)
|
fmt.Fprintf(b, "\x1b[1m\x1b[%dm\x1b[7m*DRYRUN*\x1b[0m \x1b[%dm[%s] \x1b[0m%s%s", gray, f.color, job, debugFlag, entry.Message)
|
||||||
} else {
|
} else {
|
||||||
@@ -251,7 +262,7 @@ func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
|
|||||||
debugFlag = "[DEBUG] "
|
debugFlag = "[DEBUG] "
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry.Data["raw_output"] == true {
|
if entry.Data[rawOutputField] == true {
|
||||||
fmt.Fprintf(b, "[%s] | %s", job, entry.Message)
|
fmt.Fprintf(b, "[%s] | %s", job, entry.Message)
|
||||||
} else if entry.Data["dryrun"] == true {
|
} else if entry.Data["dryrun"] == true {
|
||||||
fmt.Fprintf(b, "*DRYRUN* [%s] %s%s", job, debugFlag, entry.Message)
|
fmt.Fprintf(b, "*DRYRUN* [%s] %s%s", job, debugFlag, entry.Message)
|
||||||
|
|||||||
@@ -78,13 +78,20 @@ func (rc *RunContext) String() string {
|
|||||||
|
|
||||||
// GetEnv returns the env for the context
|
// GetEnv returns the env for the context
|
||||||
func (rc *RunContext) GetEnv() map[string]string {
|
func (rc *RunContext) GetEnv() map[string]string {
|
||||||
|
baseEnv := map[string]string{}
|
||||||
|
if rc.Run != nil && rc.Run.Workflow != nil && rc.Config != nil {
|
||||||
|
job := rc.Run.Job()
|
||||||
|
if job != nil {
|
||||||
|
baseEnv = mergeMaps(rc.Run.Workflow.Env, job.Environment(), rc.Config.Env)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if rc.Env == nil {
|
if rc.Env == nil {
|
||||||
rc.Env = map[string]string{}
|
rc.Env = map[string]string{}
|
||||||
if rc.Run != nil && rc.Run.Workflow != nil && rc.Config != nil {
|
}
|
||||||
job := rc.Run.Job()
|
for k, v := range baseEnv {
|
||||||
if job != nil {
|
if _, ok := rc.Env[k]; !ok {
|
||||||
rc.Env = mergeMaps(rc.Run.Workflow.Env, job.Environment(), rc.Config.Env)
|
rc.Env[k] = v
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rc.Env["ACT"] = "true"
|
rc.Env["ACT"] = "true"
|
||||||
|
|||||||
@@ -572,6 +572,10 @@ if: false`, ""),
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunContextGetEnv(t *testing.T) {
|
func TestRunContextGetEnv(t *testing.T) {
|
||||||
|
var jobEnv yaml.Node
|
||||||
|
err := jobEnv.Encode(map[string]string{"JOB_ONLY": "job-value"})
|
||||||
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
description string
|
description string
|
||||||
rc *RunContext
|
rc *RunContext
|
||||||
@@ -612,6 +616,26 @@ func TestRunContextGetEnv(t *testing.T) {
|
|||||||
targetEnv: "OVERWRITTEN",
|
targetEnv: "OVERWRITTEN",
|
||||||
want: "false",
|
want: "false",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Pre-populated run context env should still include job env",
|
||||||
|
rc: &RunContext{
|
||||||
|
Config: &Config{},
|
||||||
|
Env: map[string]string{
|
||||||
|
"RUNTIME_ONLY": "true",
|
||||||
|
},
|
||||||
|
Run: &model.Run{
|
||||||
|
JobID: "test",
|
||||||
|
Workflow: &model.Workflow{
|
||||||
|
Jobs: map[string]*model.Job{"test": {
|
||||||
|
Name: "test",
|
||||||
|
Env: jobEnv,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetEnv: "JOB_ONLY",
|
||||||
|
want: "job-value",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@@ -622,6 +646,37 @@ func TestRunContextGetEnv(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunContextGetEnvKeepsExpressionEvaluatorEnvCurrent(t *testing.T) {
|
||||||
|
var jobEnv yaml.Node
|
||||||
|
err := jobEnv.Encode(map[string]string{"JOB_ONLY": "job-value"})
|
||||||
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
|
rc := &RunContext{
|
||||||
|
Config: &Config{},
|
||||||
|
Env: map[string]string{
|
||||||
|
"RUNTIME_ONLY": "true",
|
||||||
|
},
|
||||||
|
Run: &model.Run{
|
||||||
|
JobID: "test",
|
||||||
|
Workflow: &model.Workflow{
|
||||||
|
Jobs: map[string]*model.Job{"test": {
|
||||||
|
Name: "test",
|
||||||
|
Env: jobEnv,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
||||||
|
rc.GetEnv()
|
||||||
|
rc.setEnv(context.Background(), map[string]string{"name": "STEP_TIMEOUT"}, "0")
|
||||||
|
|
||||||
|
got, evalErr := rc.ExprEval.evaluate(context.Background(), "env.STEP_TIMEOUT", exprparser.DefaultStatusCheckNone)
|
||||||
|
assert.NoError(t, evalErr) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
assert.EqualValues(t, "0", got)
|
||||||
|
assert.EqualValues(t, "job-value", rc.Env["JOB_ONLY"]) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
}
|
||||||
|
|
||||||
func Test_createSimpleContainerName(t *testing.T) {
|
func Test_createSimpleContainerName(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
parts []string
|
parts []string
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ func (sal *stepActionLocal) main() common.Executor {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printRunActionHeader(ctx, sal.Step, sal.env, sal.getRunContext())
|
||||||
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
|
defer rawLogger.Infof("::endgroup::")
|
||||||
|
|
||||||
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)
|
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)
|
||||||
|
|
||||||
localReader := func(ctx context.Context) actionYamlReader {
|
localReader := func(ctx context.Context) actionYamlReader {
|
||||||
|
|||||||
@@ -166,6 +166,10 @@ func (sar *stepActionRemote) main() common.Executor {
|
|||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
sar.prepareActionExecutor(),
|
sar.prepareActionExecutor(),
|
||||||
runStepExecutor(sar, stepStageMain, func(ctx context.Context) error {
|
runStepExecutor(sar, stepStageMain, func(ctx context.Context) error {
|
||||||
|
printRunActionHeader(ctx, sar.Step, sar.env, sar.RunContext)
|
||||||
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
|
defer rawLogger.Infof("::endgroup::")
|
||||||
|
|
||||||
github := sar.getGithubContext(ctx)
|
github := sar.getGithubContext(ctx)
|
||||||
if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
|
if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
|
||||||
if sar.RunContext.Config.BindWorkdir {
|
if sar.RunContext.Config.BindWorkdir {
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd, e
|
|||||||
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
|
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
|
||||||
|
|
||||||
binds, mounts := rc.GetBindsAndMounts()
|
binds, mounts := rc.GetBindsAndMounts()
|
||||||
|
networkMode := "container:" + rc.jobContainerName()
|
||||||
|
if rc.IsHostEnv(ctx) {
|
||||||
|
networkMode = "default"
|
||||||
|
}
|
||||||
stepContainer := ContainerNewContainer(&container.NewContainerInput{
|
stepContainer := ContainerNewContainer(&container.NewContainerInput{
|
||||||
Cmd: cmd,
|
Cmd: cmd,
|
||||||
Entrypoint: entrypoint,
|
Entrypoint: entrypoint,
|
||||||
@@ -126,7 +130,7 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd, e
|
|||||||
Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+step.ID),
|
Name: createSimpleContainerName(rc.jobContainerName(), "STEP-"+step.ID),
|
||||||
Env: envList,
|
Env: envList,
|
||||||
Mounts: mounts,
|
Mounts: mounts,
|
||||||
NetworkMode: "container:" + rc.jobContainerName(),
|
NetworkMode: networkMode,
|
||||||
Binds: binds,
|
Binds: binds,
|
||||||
Stdout: logWriter,
|
Stdout: logWriter,
|
||||||
Stderr: logWriter,
|
Stderr: logWriter,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/container"
|
"gitea.com/gitea/act_runner/act/container"
|
||||||
@@ -118,3 +119,81 @@ func TestStepDockerPrePost(t *testing.T) {
|
|||||||
err = sd.post()(ctx)
|
err = sd.post()(ctx)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStepDockerNewStepContainerNetworkMode(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
platform string
|
||||||
|
expectDefault bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "docker mode attaches to job container network",
|
||||||
|
platform: "node:14",
|
||||||
|
expectDefault: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host mode uses default network",
|
||||||
|
platform: "-self-hosted",
|
||||||
|
expectDefault: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cm := &containerMock{}
|
||||||
|
|
||||||
|
var captured *container.NewContainerInput
|
||||||
|
origContainerNewContainer := ContainerNewContainer
|
||||||
|
ContainerNewContainer = func(input *container.NewContainerInput) container.ExecutionsEnvironment {
|
||||||
|
captured = input
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
ContainerNewContainer = origContainerNewContainer
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
platform := tc.platform
|
||||||
|
sd := &stepDocker{
|
||||||
|
RunContext: &RunContext{
|
||||||
|
StepResults: map[string]*model.StepResult{},
|
||||||
|
Config: &Config{
|
||||||
|
PlatformPicker: func(_ []string) string {
|
||||||
|
return platform
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Run: &model.Run{
|
||||||
|
JobID: "1",
|
||||||
|
Workflow: &model.Workflow{
|
||||||
|
Jobs: map[string]*model.Job{
|
||||||
|
"1": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
JobContainer: cm,
|
||||||
|
},
|
||||||
|
Step: &model.Step{
|
||||||
|
ID: "1",
|
||||||
|
Uses: "docker://alpine:3.20",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectDefault, sd.RunContext.IsHostEnv(ctx),
|
||||||
|
"IsHostEnv mismatch for platform %q", tc.platform)
|
||||||
|
|
||||||
|
_ = sd.newStepContainer(ctx, "alpine:3.20", []string{"echo", "hello"}, nil)
|
||||||
|
|
||||||
|
if tc.expectDefault {
|
||||||
|
assert.Equal(t, "default", captured.NetworkMode,
|
||||||
|
"host-mode step container must use 'default' network, got %q",
|
||||||
|
captured.NetworkMode)
|
||||||
|
} else {
|
||||||
|
assert.True(t, strings.HasPrefix(captured.NetworkMode, "container:"),
|
||||||
|
"docker-mode step container must attach to job container network, got %q",
|
||||||
|
captured.NetworkMode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/act_runner/act/common"
|
"gitea.com/gitea/act_runner/act/common"
|
||||||
@@ -17,15 +18,18 @@ import (
|
|||||||
"gitea.com/gitea/act_runner/act/model"
|
"gitea.com/gitea/act_runner/act/model"
|
||||||
|
|
||||||
"github.com/kballard/go-shellquote"
|
"github.com/kballard/go-shellquote"
|
||||||
|
yaml "go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepRun struct {
|
type stepRun struct {
|
||||||
Step *model.Step
|
Step *model.Step
|
||||||
RunContext *RunContext
|
RunContext *RunContext
|
||||||
cmd []string
|
cmd []string
|
||||||
cmdline string
|
cmdline string
|
||||||
env map[string]string
|
env map[string]string
|
||||||
WorkingDirectory string
|
WorkingDirectory string
|
||||||
|
interpolatedScript string
|
||||||
|
shellCommand string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr *stepRun) pre() common.Executor {
|
func (sr *stepRun) pre() common.Executor {
|
||||||
@@ -39,15 +43,154 @@ func (sr *stepRun) main() common.Executor {
|
|||||||
return runStepExecutor(sr, stepStageMain, common.NewPipelineExecutor(
|
return runStepExecutor(sr, stepStageMain, common.NewPipelineExecutor(
|
||||||
sr.setupShellCommandExecutor(),
|
sr.setupShellCommandExecutor(),
|
||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
sr.getRunContext().ApplyExtraPath(ctx, &sr.env)
|
rc := sr.getRunContext()
|
||||||
if he, ok := sr.getRunContext().JobContainer.(*container.HostEnvironment); ok && he != nil {
|
// Apply ::add-path:: effects before printing so PATH is accurate in the env: block.
|
||||||
|
rc.ApplyExtraPath(ctx, &sr.env)
|
||||||
|
sr.printRunScriptActionDetails(ctx)
|
||||||
|
if he, ok := rc.JobContainer.(*container.HostEnvironment); ok && he != nil {
|
||||||
return he.ExecWithCmdLine(sr.cmd, sr.cmdline, sr.env, "", sr.WorkingDirectory)(ctx)
|
return he.ExecWithCmdLine(sr.cmd, sr.cmdline, sr.env, "", sr.WorkingDirectory)(ctx)
|
||||||
}
|
}
|
||||||
return sr.getRunContext().JobContainer.Exec(sr.cmd, sr.env, "", sr.WorkingDirectory)(ctx)
|
return rc.JobContainer.Exec(sr.cmd, sr.env, "", sr.WorkingDirectory)(ctx)
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printRunScriptActionDetails mirrors actions/runner ScriptHandler.PrintActionDetails
|
||||||
|
// for script steps.
|
||||||
|
func (sr *stepRun) printRunScriptActionDetails(ctx context.Context) {
|
||||||
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
|
scriptLineLogger := rawLogger.WithField(scriptLineCyanField, true)
|
||||||
|
|
||||||
|
normalized := strings.TrimRight(strings.ReplaceAll(sr.interpolatedScript, "\r\n", "\n"), "\n")
|
||||||
|
|
||||||
|
rawLogger.Infof("::group::Run %s", sr.runScriptGroupTitle(normalized))
|
||||||
|
|
||||||
|
if normalized != "" {
|
||||||
|
for line := range strings.SplitSeq(normalized, "\n") {
|
||||||
|
scriptLineLogger.Info(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rawLogger.Infof("shell: %s", sr.shellCommand)
|
||||||
|
|
||||||
|
printStepEnvBlock(ctx, sr.Step, sr.env, sr.getRunContext())
|
||||||
|
rawLogger.Infof("::endgroup::")
|
||||||
|
}
|
||||||
|
|
||||||
|
// printRunActionHeader mirrors actions/runner's "Run <action>" header for `uses:` steps,
|
||||||
|
// including the with: inputs and the step-level env: block. The caller is responsible
|
||||||
|
// for emitting ::endgroup:: after the action finishes.
|
||||||
|
func printRunActionHeader(ctx context.Context, step *model.Step, env map[string]string, rc *RunContext) {
|
||||||
|
if step == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
|
|
||||||
|
title := step.Uses
|
||||||
|
if step.Name != "" {
|
||||||
|
title = step.Name
|
||||||
|
}
|
||||||
|
rawLogger.Infof("::group::Run %s", title)
|
||||||
|
|
||||||
|
if len(step.With) > 0 {
|
||||||
|
rawLogger.Infof("with:")
|
||||||
|
for _, k := range slices.Sorted(maps.Keys(step.With)) {
|
||||||
|
rawLogger.Infof(" %s: %s", k, step.With[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printStepEnvBlock(ctx, step, env, rc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// printStepEnvBlock emits the declared-env block (YAML order, internal vars filtered)
|
||||||
|
// shared by the run: and uses: "Run" headers.
|
||||||
|
func printStepEnvBlock(ctx context.Context, step *model.Step, env map[string]string, rc *RunContext) {
|
||||||
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
|
caseInsensitive := rc != nil && rc.JobContainer != nil && rc.JobContainer.IsEnvironmentCaseInsensitive()
|
||||||
|
var visible []string
|
||||||
|
for _, k := range stepDeclaredEnvKeysInOrder(step) {
|
||||||
|
if !isInternalEnvKey(k, caseInsensitive) {
|
||||||
|
visible = append(visible, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(visible) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rawLogger.Infof("env:")
|
||||||
|
envLookup := env
|
||||||
|
if caseInsensitive {
|
||||||
|
envLookup = make(map[string]string, len(env))
|
||||||
|
for k, v := range env {
|
||||||
|
envLookup[strings.ToUpper(k)] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, k := range visible {
|
||||||
|
lookupKey := k
|
||||||
|
if caseInsensitive {
|
||||||
|
lookupKey = strings.ToUpper(k)
|
||||||
|
}
|
||||||
|
rawLogger.Infof(" %s: %s", k, envLookup[lookupKey])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isInternalEnvKey matches actions/runner's filtered set of vars that are hidden
|
||||||
|
// from the "Run" header's env: block because they are injected by the runner itself.
|
||||||
|
func isInternalEnvKey(k string, caseInsensitive bool) bool {
|
||||||
|
upper := k
|
||||||
|
if caseInsensitive {
|
||||||
|
upper = strings.ToUpper(k)
|
||||||
|
}
|
||||||
|
switch upper {
|
||||||
|
case "PATH", "HOME", "CI":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(upper, "GITHUB_") ||
|
||||||
|
strings.HasPrefix(upper, "GITEA_") ||
|
||||||
|
strings.HasPrefix(upper, "RUNNER_") ||
|
||||||
|
strings.HasPrefix(upper, "INPUT_")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sr *stepRun) runScriptGroupTitle(normalizedScript string) string {
|
||||||
|
trimmed := strings.TrimLeft(normalizedScript, " \t\r\n")
|
||||||
|
if idx := strings.IndexAny(trimmed, "\r\n"); idx >= 0 {
|
||||||
|
trimmed = trimmed[:idx]
|
||||||
|
}
|
||||||
|
if trimmed != "" {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
if sr.Step != nil {
|
||||||
|
if sr.Step.Name != "" {
|
||||||
|
return sr.Step.Name
|
||||||
|
}
|
||||||
|
return sr.Step.ID
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// stepDeclaredEnvKeysInOrder walks the raw YAML Env mapping so keys are emitted in
|
||||||
|
// the order the workflow author wrote them; step.Environment() decodes into a Go map
|
||||||
|
// and loses ordering.
|
||||||
|
func stepDeclaredEnvKeysInOrder(step *model.Step) []string {
|
||||||
|
if step == nil || step.Env.Kind != yaml.MappingNode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
content := step.Env.Content
|
||||||
|
keys := make([]string, 0, len(content)/2)
|
||||||
|
seen := make(map[string]struct{}, len(content)/2)
|
||||||
|
for i := 0; i+1 < len(content); i += 2 {
|
||||||
|
k := content[i]
|
||||||
|
if k.Kind != yaml.ScalarNode || k.Tag == "!!merge" || k.Value == "<<" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, dup := seen[k.Value]; dup {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[k.Value] = struct{}{}
|
||||||
|
keys = append(keys, k.Value)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
func (sr *stepRun) post() common.Executor {
|
func (sr *stepRun) post() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
@@ -111,8 +254,10 @@ func (sr *stepRun) setupShellCommand(ctx context.Context) (name, script string,
|
|||||||
step := sr.Step
|
step := sr.Step
|
||||||
|
|
||||||
script = sr.RunContext.NewStepExpressionEvaluator(ctx, sr).Interpolate(ctx, step.Run)
|
script = sr.RunContext.NewStepExpressionEvaluator(ctx, sr).Interpolate(ctx, step.Run)
|
||||||
|
sr.interpolatedScript = script
|
||||||
|
|
||||||
scCmd := step.ShellCommand()
|
scCmd := step.ShellCommand()
|
||||||
|
sr.shellCommand = scCmd
|
||||||
|
|
||||||
name = getScriptName(sr.RunContext, step)
|
name = getScriptName(sr.RunContext, step)
|
||||||
|
|
||||||
|
|||||||
182
act/runner/step_run_print_test.go
Normal file
182
act/runner/step_run_print_test.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2026 The nektos/act Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.com/gitea/act_runner/act/common"
|
||||||
|
"gitea.com/gitea/act_runner/act/model"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
yaml "go.yaml.in/yaml/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunScriptGroupTitle(t *testing.T) {
|
||||||
|
sr := &stepRun{Step: &model.Step{Name: "Build"}}
|
||||||
|
assert.Equal(t, "make build", sr.runScriptGroupTitle("make build"))
|
||||||
|
assert.Equal(t, "echo one", sr.runScriptGroupTitle(" \techo one\necho two"))
|
||||||
|
assert.Equal(t, "Build", sr.runScriptGroupTitle(""))
|
||||||
|
|
||||||
|
sr = &stepRun{Step: &model.Step{ID: "s1"}}
|
||||||
|
assert.Equal(t, "s1", sr.runScriptGroupTitle("\n \n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepDeclaredEnvOrderPreservesYAML(t *testing.T) {
|
||||||
|
raw := `id: s1
|
||||||
|
run: "echo 1"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: tok
|
||||||
|
PATH: /custom/bin
|
||||||
|
MY_VAR: hello
|
||||||
|
`
|
||||||
|
var step model.Step
|
||||||
|
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
|
||||||
|
assert.Equal(t, []string{"GITHUB_TOKEN", "PATH", "MY_VAR"}, stepDeclaredEnvKeysInOrder(&step))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepDeclaredEnvKeysInOrderEmpty(t *testing.T) {
|
||||||
|
assert.Nil(t, stepDeclaredEnvKeysInOrder(nil))
|
||||||
|
assert.Empty(t, stepDeclaredEnvKeysInOrder(&model.Step{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepDeclaredEnvKeysIgnoreYAMLMergeKey(t *testing.T) {
|
||||||
|
doc := `
|
||||||
|
common: &common
|
||||||
|
COMMON_A: a
|
||||||
|
COMMON_B: b
|
||||||
|
step:
|
||||||
|
env:
|
||||||
|
LOCAL_BEFORE: before
|
||||||
|
<<: *common
|
||||||
|
COMMON_B: overridden
|
||||||
|
LOCAL_AFTER: after
|
||||||
|
`
|
||||||
|
var root struct {
|
||||||
|
Step model.Step `yaml:"step"`
|
||||||
|
}
|
||||||
|
require.NoError(t, yaml.Unmarshal([]byte(doc), &root))
|
||||||
|
|
||||||
|
keys := stepDeclaredEnvKeysInOrder(&root.Step)
|
||||||
|
assert.Equal(t, []string{"LOCAL_BEFORE", "COMMON_B", "LOCAL_AFTER"}, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintRunScriptActionDetailsGolden(t *testing.T) {
|
||||||
|
raw := `id: s1
|
||||||
|
name: Build
|
||||||
|
run: |
|
||||||
|
echo one
|
||||||
|
echo two
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
PATH_PREFIX: /custom/bin
|
||||||
|
GITHUB_TOKEN: tok
|
||||||
|
GREETING: hello
|
||||||
|
`
|
||||||
|
var step model.Step
|
||||||
|
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
logger := logrus.New()
|
||||||
|
logger.SetOutput(buf)
|
||||||
|
logger.SetLevel(logrus.InfoLevel)
|
||||||
|
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
||||||
|
entry := logger.WithFields(logrus.Fields{"job": "j1"})
|
||||||
|
ctx := common.WithLogger(context.Background(), entry)
|
||||||
|
|
||||||
|
sr := &stepRun{
|
||||||
|
Step: &step,
|
||||||
|
RunContext: &RunContext{},
|
||||||
|
shellCommand: "pwsh -command . '{0}'",
|
||||||
|
interpolatedScript: "echo one\necho two\n",
|
||||||
|
env: map[string]string{
|
||||||
|
"PATH_PREFIX": "/custom/bin",
|
||||||
|
"GITHUB_TOKEN": "tok",
|
||||||
|
"GREETING": "hello",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sr.printRunScriptActionDetails(ctx)
|
||||||
|
|
||||||
|
want := strings.Join([]string{
|
||||||
|
"[j1] | ::group::Run echo one",
|
||||||
|
"[j1] | echo one",
|
||||||
|
"[j1] | echo two",
|
||||||
|
"[j1] | shell: pwsh -command . '{0}'",
|
||||||
|
"[j1] | env:",
|
||||||
|
"[j1] | PATH_PREFIX: /custom/bin",
|
||||||
|
"[j1] | GREETING: hello",
|
||||||
|
"[j1] | ::endgroup::",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
assert.Equal(t, want, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintRunActionHeaderGolden(t *testing.T) {
|
||||||
|
raw := `id: s1
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: "0"
|
||||||
|
token: secret
|
||||||
|
env:
|
||||||
|
CUSTOM: value
|
||||||
|
GITHUB_TOKEN: tok
|
||||||
|
`
|
||||||
|
var step model.Step
|
||||||
|
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
logger := logrus.New()
|
||||||
|
logger.SetOutput(buf)
|
||||||
|
logger.SetLevel(logrus.InfoLevel)
|
||||||
|
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
||||||
|
entry := logger.WithFields(logrus.Fields{"job": "j1"})
|
||||||
|
ctx := common.WithLogger(context.Background(), entry)
|
||||||
|
|
||||||
|
printRunActionHeader(ctx, &step, map[string]string{"CUSTOM": "value", "GITHUB_TOKEN": "tok"}, &RunContext{})
|
||||||
|
|
||||||
|
want := strings.Join([]string{
|
||||||
|
"[j1] | ::group::Run actions/checkout@v4",
|
||||||
|
"[j1] | with:",
|
||||||
|
"[j1] | fetch-depth: 0",
|
||||||
|
"[j1] | token: secret",
|
||||||
|
"[j1] | env:",
|
||||||
|
"[j1] | CUSTOM: value",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
assert.Equal(t, want, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsInternalEnvKey(t *testing.T) {
|
||||||
|
for _, k := range []string{"PATH", "HOME", "CI", "GITHUB_TOKEN", "GITEA_ACTIONS", "RUNNER_OS", "INPUT_FOO"} {
|
||||||
|
assert.True(t, isInternalEnvKey(k, false), k)
|
||||||
|
}
|
||||||
|
for _, k := range []string{"PATH_PREFIX", "MY_VAR", "GREETING", "HOMEPAGE"} {
|
||||||
|
assert.False(t, isInternalEnvKey(k, false), k)
|
||||||
|
}
|
||||||
|
assert.True(t, isInternalEnvKey("path", true))
|
||||||
|
assert.False(t, isInternalEnvKey("path", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintColoredScriptLineCyan(t *testing.T) {
|
||||||
|
f := &jobLogFormatter{color: cyan}
|
||||||
|
entry := &logrus.Entry{
|
||||||
|
Level: logrus.InfoLevel,
|
||||||
|
Message: "echo one",
|
||||||
|
Data: logrus.Fields{
|
||||||
|
"job": "j1",
|
||||||
|
rawOutputField: true,
|
||||||
|
scriptLineCyanField: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
f.printColored(buf, entry)
|
||||||
|
assert.Equal(t, "\x1b[36m|\x1b[0m \x1b[36;1mecho one\x1b[0m", buf.String())
|
||||||
|
}
|
||||||
@@ -120,6 +120,10 @@ func TestSetupEnv(t *testing.T) {
|
|||||||
cm := &containerMock{}
|
cm := &containerMock{}
|
||||||
sm := &stepMock{}
|
sm := &stepMock{}
|
||||||
|
|
||||||
|
var jobEnv yaml.Node
|
||||||
|
err := jobEnv.Encode(map[string]string{"JOB_KEY": "jobvalue"})
|
||||||
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
Env: map[string]string{
|
Env: map[string]string{
|
||||||
@@ -131,9 +135,7 @@ func TestSetupEnv(t *testing.T) {
|
|||||||
Workflow: &model.Workflow{
|
Workflow: &model.Workflow{
|
||||||
Jobs: map[string]*model.Job{
|
Jobs: map[string]*model.Job{
|
||||||
"1": {
|
"1": {
|
||||||
Env: yaml.Node{
|
Env: jobEnv,
|
||||||
Value: "JOB_KEY: jobvalue",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -155,7 +157,7 @@ func TestSetupEnv(t *testing.T) {
|
|||||||
sm.On("getStepModel").Return(step)
|
sm.On("getStepModel").Return(step)
|
||||||
sm.On("getEnv").Return(&env)
|
sm.On("getEnv").Return(&env)
|
||||||
|
|
||||||
err := setupEnv(context.Background(), sm)
|
err = setupEnv(context.Background(), sm)
|
||||||
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.Nil(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
// These are commit or system specific
|
// These are commit or system specific
|
||||||
@@ -191,6 +193,7 @@ func TestSetupEnv(t *testing.T) {
|
|||||||
"GITHUB_SERVER_URL": "https://",
|
"GITHUB_SERVER_URL": "https://",
|
||||||
"GITHUB_WORKFLOW": "",
|
"GITHUB_WORKFLOW": "",
|
||||||
"INPUT_STEP_WITH": "with-value",
|
"INPUT_STEP_WITH": "with-value",
|
||||||
|
"JOB_KEY": "jobvalue",
|
||||||
"RC_KEY": "rcvalue",
|
"RC_KEY": "rcvalue",
|
||||||
"RUNNER_PERFLOG": "/dev/null",
|
"RUNNER_PERFLOG": "/dev/null",
|
||||||
"RUNNER_TRACKING_ID": "",
|
"RUNNER_TRACKING_ID": "",
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -114,7 +114,3 @@ require (
|
|||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remove after github.com/docker/distribution is updated to support distribution/reference v0.6.0
|
|
||||||
// (pulled in via moby/buildkit, breaks on undefined: reference.SplitHostname)
|
|
||||||
replace github.com/distribution/reference v0.6.0 => github.com/distribution/reference v0.5.0
|
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -1,7 +1,5 @@
|
|||||||
code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLrKfls=
|
code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLrKfls=
|
||||||
code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
|
code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
|
||||||
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
|
|
||||||
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
|
||||||
connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
|
connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
|
||||||
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
||||||
cyphar.com/go-pathrs v0.2.3 h1:0pH8gep37wB0BgaXrEaN1OtZhUMeS7VvaejSr6i822o=
|
cyphar.com/go-pathrs v0.2.3 h1:0pH8gep37wB0BgaXrEaN1OtZhUMeS7VvaejSr6i822o=
|
||||||
@@ -51,10 +49,8 @@ github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/cli v25.0.3+incompatible h1:KLeNs7zws74oFuVhgZQ5ONGZiXUUdgsdy6/EsX/6284=
|
|
||||||
github.com/docker/cli v25.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
|
||||||
github.com/docker/cli v25.0.7+incompatible h1:scW/AbGafKmANsonsFckFHTwpz2QypoPA/zpoLnDs/E=
|
github.com/docker/cli v25.0.7+incompatible h1:scW/AbGafKmANsonsFckFHTwpz2QypoPA/zpoLnDs/E=
|
||||||
github.com/docker/cli v25.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v25.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/docker v25.0.13+incompatible h1:YeBrkUd3q0ZoRDNoEzuopwCLU+uD8GZahDHwBdsTnkU=
|
github.com/docker/docker v25.0.13+incompatible h1:YeBrkUd3q0ZoRDNoEzuopwCLU+uD8GZahDHwBdsTnkU=
|
||||||
|
|||||||
Reference in New Issue
Block a user