mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-05-07 15:53:24 +02:00
Align step failure log output with GitHub Actions (#927)
Fixes #926. Before: <img src="/attachments/a5ae9221-eee2-410a-964e-6103ce126df4" alt="image.png" width="400"> After: <img width="400" alt="image.png" src="attachments/2f2d67c4-6080-4ec3-9ae5-df33e6479920"> Also gets rid of a bunch of emojis in the logging and the obsolete link to `nektos/act` and align some other error messages. --- This PR was written with the help of Claude Opus 4.7 --------- Co-authored-by: Nicolas <bircni@icloud.com> Reviewed-on: https://gitea.com/gitea/runner/pulls/927 Reviewed-by: Nicolas <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io> Co-committed-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -6,6 +6,7 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
@@ -13,6 +14,13 @@ import (
|
|||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ExitCodeError reports a non-zero process exit code from a container command.
|
||||||
|
type ExitCodeError int
|
||||||
|
|
||||||
|
func (e ExitCodeError) Error() string {
|
||||||
|
return fmt.Sprintf("Process completed with exit code %d.", int(e))
|
||||||
|
}
|
||||||
|
|
||||||
// NewContainerInput the input for the New function
|
// NewContainerInput the input for the New function
|
||||||
type NewContainerInput struct {
|
type NewContainerInput struct {
|
||||||
Image string
|
Image string
|
||||||
|
|||||||
@@ -633,14 +633,10 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
|
|||||||
return fmt.Errorf("failed to inspect exec: %w", err)
|
return fmt.Errorf("failed to inspect exec: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch inspectResp.ExitCode {
|
if inspectResp.ExitCode == 0 {
|
||||||
case 0:
|
|
||||||
return nil
|
return nil
|
||||||
case 127:
|
|
||||||
return fmt.Errorf("exitcode '%d': command not found, please refer to https://github.com/nektos/act/issues/107 for more information", inspectResp.ExitCode)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("exitcode '%d': failure", inspectResp.ExitCode)
|
|
||||||
}
|
}
|
||||||
|
return ExitCodeError(inspectResp.ExitCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -930,7 +926,7 @@ func (cr *containerReference) wait() common.Executor {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
|
return ExitCodeError(statusCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/sirupsen/logrus/hooks/test"
|
"github.com/sirupsen/logrus/hooks/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDocker(t *testing.T) {
|
func TestDocker(t *testing.T) {
|
||||||
@@ -85,6 +86,11 @@ func (m *mockDockerClient) ContainerExecInspect(ctx context.Context, execID stri
|
|||||||
return args.Get(0).(types.ContainerExecInspect), args.Error(1)
|
return args.Get(0).(types.ContainerExecInspect), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockDockerClient) ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
|
||||||
|
args := m.Called(ctx, containerID, condition)
|
||||||
|
return args.Get(0).(<-chan container.WaitResponse), args.Get(1).(<-chan error)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockDockerClient) CopyToContainer(ctx context.Context, id, path string, content io.Reader, options types.CopyToContainerOptions) error {
|
func (m *mockDockerClient) CopyToContainer(ctx context.Context, id, path string, content io.Reader, options types.CopyToContainerOptions) error {
|
||||||
args := m.Called(ctx, id, path, content, options)
|
args := m.Called(ctx, id, path, content, options)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
@@ -174,12 +180,43 @@ func TestDockerExecFailure(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := cr.exec([]string{""}, map[string]string{}, "user", "workdir")(ctx)
|
err := cr.exec([]string{""}, map[string]string{}, "user", "workdir")(ctx)
|
||||||
assert.Error(t, err, "exit with `FAILURE`: 1") //nolint:testifylint // pre-existing issue from nektos/act
|
var exitErr ExitCodeError
|
||||||
|
require.ErrorAs(t, err, &exitErr)
|
||||||
|
assert.Equal(t, ExitCodeError(1), exitErr)
|
||||||
|
assert.Equal(t, "Process completed with exit code 1.", err.Error())
|
||||||
|
|
||||||
conn.AssertExpectations(t)
|
conn.AssertExpectations(t)
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDockerWaitFailure(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
statusCh := make(chan container.WaitResponse, 1)
|
||||||
|
statusCh <- container.WaitResponse{StatusCode: 2}
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
|
client := &mockDockerClient{}
|
||||||
|
client.On("ContainerWait", ctx, "123", container.WaitConditionNotRunning).
|
||||||
|
Return((<-chan container.WaitResponse)(statusCh), (<-chan error)(errCh))
|
||||||
|
|
||||||
|
cr := &containerReference{
|
||||||
|
id: "123",
|
||||||
|
cli: client,
|
||||||
|
input: &NewContainerInput{
|
||||||
|
Image: "image",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cr.wait()(ctx)
|
||||||
|
var exitErr ExitCodeError
|
||||||
|
require.ErrorAs(t, err, &exitErr)
|
||||||
|
assert.Equal(t, ExitCodeError(2), exitErr)
|
||||||
|
assert.Equal(t, "Process completed with exit code 2.", err.Error())
|
||||||
|
|
||||||
|
client.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestDockerCopyTarStream(t *testing.T) {
|
func TestDockerCopyTarStream(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
@@ -372,6 +372,10 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
|
|||||||
}
|
}
|
||||||
err = cmd.Wait()
|
err = cmd.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if errors.As(err, &exitErr) {
|
||||||
|
return ExitCodeError(exitErr.ExitCode())
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if tty != nil {
|
if tty != nil {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
@@ -74,6 +75,31 @@ func TestGetContainerArchive(t *testing.T) {
|
|||||||
assert.ErrorIs(t, err, io.EOF)
|
assert.ErrorIs(t, err, io.EOF)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHostEnvironmentExecExitCode(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("uses POSIX shell")
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
ctx := context.Background()
|
||||||
|
e := &HostEnvironment{
|
||||||
|
Path: filepath.Join(dir, "path"),
|
||||||
|
TmpDir: filepath.Join(dir, "tmp"),
|
||||||
|
ToolCache: filepath.Join(dir, "tool_cache"),
|
||||||
|
ActPath: filepath.Join(dir, "act_path"),
|
||||||
|
StdOut: io.Discard,
|
||||||
|
Workdir: filepath.Join(dir, "path"),
|
||||||
|
}
|
||||||
|
for _, p := range []string{e.Path, e.TmpDir, e.ToolCache, e.ActPath} {
|
||||||
|
assert.NoError(t, os.MkdirAll(p, 0o700)) //nolint:testifylint // test setup
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.Exec([]string{"sh", "-c", "exit 3"}, map[string]string{"PATH": os.Getenv("PATH")}, "", "")(ctx)
|
||||||
|
var exitErr ExitCodeError
|
||||||
|
require.ErrorAs(t, err, &exitErr)
|
||||||
|
assert.Equal(t, ExitCodeError(3), exitErr)
|
||||||
|
assert.Equal(t, "Process completed with exit code 3.", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) {
|
func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) {
|
||||||
logger := logrus.New()
|
logger := logrus.New()
|
||||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ type jobInfo interface {
|
|||||||
result(result string)
|
result(result string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reportStepError emits the GitHub Actions ##[error] annotation and records
|
||||||
|
// the error against the job so the job is reported as failed.
|
||||||
|
func reportStepError(ctx context.Context, err error) {
|
||||||
|
common.Logger(ctx).Errorf("##[error]%v", err)
|
||||||
|
common.SetJobError(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executor {
|
func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executor {
|
||||||
steps := make([]common.Executor, 0)
|
steps := make([]common.Executor, 0)
|
||||||
preSteps := make([]common.Executor, 0)
|
preSteps := make([]common.Executor, 0)
|
||||||
@@ -32,7 +39,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
|||||||
steps = append(steps, func(ctx context.Context) error {
|
steps = append(steps, func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
if len(info.matrix()) > 0 {
|
if len(info.matrix()) > 0 {
|
||||||
logger.Infof("\U0001F9EA Matrix: %v", info.matrix())
|
logger.Infof("Matrix: %v", info.matrix())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -75,33 +82,36 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
|||||||
|
|
||||||
preExec := step.pre()
|
preExec := step.pre()
|
||||||
preSteps = append(preSteps, useStepLogger(rc, stepModel, stepStagePre, func(ctx context.Context) error {
|
preSteps = append(preSteps, useStepLogger(rc, stepModel, stepStagePre, func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
|
||||||
preErr := preExec(ctx)
|
preErr := preExec(ctx)
|
||||||
if preErr != nil {
|
if preErr != nil {
|
||||||
logger.Errorf("%v", preErr)
|
reportStepError(ctx, preErr)
|
||||||
common.SetJobError(ctx, preErr)
|
|
||||||
} else if ctx.Err() != nil {
|
} else if ctx.Err() != nil {
|
||||||
logger.Errorf("%v", ctx.Err())
|
reportStepError(ctx, ctx.Err())
|
||||||
common.SetJobError(ctx, ctx.Err())
|
|
||||||
}
|
}
|
||||||
return preErr
|
return preErr
|
||||||
}))
|
}))
|
||||||
|
|
||||||
stepExec := step.main()
|
stepExec := step.main()
|
||||||
steps = append(steps, useStepLogger(rc, stepModel, stepStageMain, func(ctx context.Context) error {
|
steps = append(steps, useStepLogger(rc, stepModel, stepStageMain, func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
|
||||||
err := stepExec(ctx)
|
err := stepExec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("%v", err)
|
reportStepError(ctx, err)
|
||||||
common.SetJobError(ctx, err)
|
|
||||||
} else if ctx.Err() != nil {
|
} else if ctx.Err() != nil {
|
||||||
logger.Errorf("%v", ctx.Err())
|
reportStepError(ctx, ctx.Err())
|
||||||
common.SetJobError(ctx, ctx.Err())
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
postExec := useStepLogger(rc, stepModel, stepStagePost, step.post())
|
postFn := step.post()
|
||||||
|
postExec := useStepLogger(rc, stepModel, stepStagePost, func(ctx context.Context) error {
|
||||||
|
err := postFn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
reportStepError(ctx, err)
|
||||||
|
} else if ctx.Err() != nil {
|
||||||
|
reportStepError(ctx, ctx.Err())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
if postExecutor != nil {
|
if postExecutor != nil {
|
||||||
// run the post executor in reverse order
|
// run the post executor in reverse order
|
||||||
postExecutor = postExec.Finally(postExecutor)
|
postExecutor = postExec.Finally(postExecutor)
|
||||||
@@ -196,7 +206,7 @@ func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success boo
|
|||||||
jobResultMessage = "failed"
|
jobResultMessage = "failed"
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.WithField("jobResult", jobResult).Infof("\U0001F3C1 Job %s", jobResultMessage)
|
logger.WithField("jobResult", jobResult).Infof("Job %s", jobResultMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setJobOutputs(ctx context.Context, rc *RunContext) {
|
func setJobOutputs(ctx context.Context, rc *RunContext) {
|
||||||
|
|||||||
@@ -730,7 +730,7 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
|
|||||||
jobType, jobTypeErr := job.Type()
|
jobType, jobTypeErr := job.Type()
|
||||||
|
|
||||||
if runJobErr != nil {
|
if runJobErr != nil {
|
||||||
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", job.If.Value, runJobErr)
|
return false, fmt.Errorf("if-expression %q evaluation failed: %s", job.If.Value, runJobErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if jobType == model.JobTypeInvalid {
|
if jobType == model.JobTypeInvalid {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
|||||||
if strings.Contains(stepString, "::add-mask::") {
|
if strings.Contains(stepString, "::add-mask::") {
|
||||||
stepString = "add-mask command"
|
stepString = "add-mask command"
|
||||||
}
|
}
|
||||||
logger.Infof("\u2B50 Run %s %s", stage, stepString)
|
logger.Infof("Run %s %s", stage, stepString)
|
||||||
|
|
||||||
// Prepare and clean Runner File Commands
|
// Prepare and clean Runner File Commands
|
||||||
actPath := rc.JobContainer.GetActPath()
|
actPath := rc.JobContainer.GetActPath()
|
||||||
@@ -158,7 +158,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
|||||||
err = executor(timeoutctx)
|
err = executor(timeoutctx)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.WithField("stepResult", stepResult.Outcome).Infof(" \u2705 Success - %s %s", stage, stepString)
|
logger.WithField("stepResult", stepResult.Outcome).Infof("Success - %s %s", stage, stepString)
|
||||||
} else {
|
} else {
|
||||||
stepResult.Outcome = model.StepStatusFailure
|
stepResult.Outcome = model.StepStatusFailure
|
||||||
|
|
||||||
@@ -169,6 +169,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if continueOnError {
|
if continueOnError {
|
||||||
|
logger.Errorf("##[error]%v", err)
|
||||||
logger.Infof("Failed but continue next step")
|
logger.Infof("Failed but continue next step")
|
||||||
err = nil
|
err = nil
|
||||||
stepResult.Conclusion = model.StepStatusSuccess
|
stepResult.Conclusion = model.StepStatusSuccess
|
||||||
@@ -176,7 +177,9 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
|||||||
stepResult.Conclusion = model.StepStatusFailure
|
stepResult.Conclusion = model.StepStatusFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.WithField("stepResult", stepResult.Outcome).Errorf(" \u274C Failure - %s %s", stage, stepString)
|
// Infof: Errorf entries are promoted to the user log by the reporter,
|
||||||
|
// which would duplicate the ##[error] annotation emitted elsewhere.
|
||||||
|
logger.WithField("stepResult", stepResult.Outcome).Infof("Failure - %s %s", stage, stepString)
|
||||||
}
|
}
|
||||||
// Process Runner File Commands
|
// Process Runner File Commands
|
||||||
orgerr := err
|
orgerr := err
|
||||||
@@ -268,7 +271,7 @@ func isStepEnabled(ctx context.Context, expr string, step step, stage stepStage)
|
|||||||
|
|
||||||
runStep, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, defaultStatusCheck)
|
runStep, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, defaultStatusCheck)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf(" \u274C Error in if-expression: \"if: %s\" (%s)", expr, err)
|
return false, fmt.Errorf("if-expression %q evaluation failed: %s", expr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return runStep, nil
|
return runStep, nil
|
||||||
@@ -284,7 +287,7 @@ func isContinueOnError(ctx context.Context, expr string, step step, _ stepStage)
|
|||||||
|
|
||||||
continueOnError, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, exprparser.DefaultStatusCheckNone)
|
continueOnError, err := EvalBool(ctx, rc.NewStepExpressionEvaluator(ctx, step), expr, exprparser.DefaultStatusCheckNone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf(" \u274C Error in continue-on-error-expression: \"continue-on-error: %s\" (%s)", expr, err)
|
return false, fmt.Errorf("continue-on-error expression %q evaluation failed: %s", expr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return continueOnError, nil
|
return continueOnError, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user