mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-11-02 13:27:16 +08:00
fix: handle workflow commands like GitHub's runner (#1333)
Commands the runner applies itself, like the `::set-output::` older actions still print, showed up raw in the job log. The reporter also re-parsed every line with one job-wide stop-commands and echo state, while act keeps one command handler per step, stage, composite child and hook, as GitHub does. A `::stop-commands::` left active in one of them kept the reporter suppressed for the others, so their `::add-mask::` values were never registered, and post steps logged command lines twice. - act's command handler decides what reaches the log, as GitHub's runner does: applied commands only under `ACTIONS_STEP_DEBUG` or `::echo::on`, `::stop-commands::` and its resume line shown with long tokens masked - Masks go straight to the reporter, which no longer parses command lines - One parser follows GitHub's rules: leading whitespace, `##[...]` anywhere in a line, registered commands only - Service logs honour only `::add-mask::` --------- Co-authored-by: bircni <bircni@icloud.com> Reviewed-on: https://gitea.com/gitea/runner/pulls/1333 Reviewed-by: bircni <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -5,6 +5,7 @@ package report
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
@@ -53,7 +54,11 @@ func TestGlobalMasks(t *testing.T) {
|
||||
|
||||
// A workflow could otherwise mask "error" here and rewrite every other task's log.
|
||||
third := register("s3cr3t", "***")
|
||||
third.addMask("runtime-secret")
|
||||
var writers sync.WaitGroup
|
||||
for _, mask := range []string{"runtime-secret", "another-secret"} {
|
||||
writers.Go(func() { third.AddMask(mask) })
|
||||
}
|
||||
writers.Wait()
|
||||
assert.Contains(t, format("saw runtime-secret"), "runtime-secret")
|
||||
assert.NotContains(t, third.mask("saw runtime-secret"), "runtime-secret")
|
||||
assert.Equal(t, "saw *** ***", third.mask("saw runtime-secret another-secret"))
|
||||
}
|
||||
|
||||
@@ -59,9 +59,8 @@ func TestJobLog_MirrorsUploadedRows(t *testing.T) {
|
||||
}))
|
||||
}
|
||||
fire("the token is s3cret-value")
|
||||
fire("::add-mask::dyn4mic-value")
|
||||
reporter.AddMask("dyn4mic-value")
|
||||
fire("and dyn4mic-value too")
|
||||
fire("::debug::suppressed unless ACTIONS_STEP_DEBUG")
|
||||
require.NoError(t, reporter.Close(""))
|
||||
|
||||
job := readJobLog(t, reporter.jobLog)
|
||||
@@ -69,8 +68,6 @@ func TestJobLog_MirrorsUploadedRows(t *testing.T) {
|
||||
assert.Contains(t, job, "and *** too")
|
||||
assert.NotContains(t, job, "s3cret-value")
|
||||
assert.NotContains(t, job, "dyn4mic-value")
|
||||
assert.NotContains(t, job, "add-mask", "the row carrying the secret never reaches the log")
|
||||
assert.NotContains(t, job, "suppressed unless", "the file holds only what was sent")
|
||||
assert.NotRegexp(t, `(?m)^\S+Z ?$`, job, "the empty row Gitea needs is not job output")
|
||||
assert.Contains(t, job, "[runner] task 41 finished: failure")
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -101,9 +100,6 @@ type Reporter struct {
|
||||
logNotify chan struct{} // signal: new log rows arrived
|
||||
stateNotify chan struct{} // signal: step transition (start/stop)
|
||||
|
||||
debugOutputEnabled bool
|
||||
stopCommandEndToken string
|
||||
|
||||
jobLog *jobLog // this task's rows on the runner's own disk, nil when log.job.dir is unset
|
||||
}
|
||||
|
||||
@@ -156,10 +152,6 @@ func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.C
|
||||
|
||||
registerGlobalMasks(rv)
|
||||
|
||||
if task.Secrets["ACTIONS_STEP_DEBUG"] == "true" {
|
||||
rv.debugOutputEnabled = true
|
||||
}
|
||||
|
||||
return rv
|
||||
}
|
||||
|
||||
@@ -345,11 +337,8 @@ func (h *reporterHandler) WithAttrs(attrs []logging.Attr) logging.Handler {
|
||||
}
|
||||
|
||||
// appendLogRow masks a row before buffering it for Gitea and the local job.log, the one point
|
||||
// feeding both. A nil row is one the command handler dropped. Caller holds stateMu.
|
||||
// feeding both. Caller holds stateMu.
|
||||
func (r *Reporter) appendLogRow(row *runnerv1.LogRow) {
|
||||
if row == nil {
|
||||
return
|
||||
}
|
||||
row.Content = r.mask(row.Content)
|
||||
if step := r.currentReportedStep; step != nil {
|
||||
if step.LogLength == 0 {
|
||||
@@ -461,7 +450,7 @@ func (r *Reporter) Handle(_ context.Context, entry logging.Record) error {
|
||||
}
|
||||
}
|
||||
if r.shouldAppendLogRow(entry) {
|
||||
r.appendLogRow(r.parseLogRow(entry))
|
||||
r.appendLogRow(logRow(entry))
|
||||
}
|
||||
r.unlockAndNotify(urgentState)
|
||||
return nil
|
||||
@@ -475,7 +464,7 @@ func (r *Reporter) Handle(_ context.Context, entry logging.Record) error {
|
||||
}
|
||||
if step == nil {
|
||||
if r.shouldAppendLogRow(entry) {
|
||||
r.appendLogRow(r.parseLogRow(entry))
|
||||
r.appendLogRow(logRow(entry))
|
||||
}
|
||||
r.unlockAndNotify(false)
|
||||
return nil
|
||||
@@ -484,9 +473,6 @@ func (r *Reporter) Handle(_ context.Context, entry logging.Record) error {
|
||||
if step.StartedAt == nil {
|
||||
step.StartedAt = timestamppb.New(timestamp)
|
||||
urgentState = true
|
||||
// The runner's own handler is per step, so an unresumed ::stop-commands:: must not
|
||||
// leave the reporter suppressed, and no longer registering masks, for the whole job.
|
||||
r.stopCommandEndToken = ""
|
||||
}
|
||||
|
||||
// Force reporting log errors as raw output to prevent silent failures
|
||||
@@ -497,16 +483,14 @@ func (r *Reporter) Handle(_ context.Context, entry logging.Record) error {
|
||||
|
||||
if ok {
|
||||
if rawOutput, ok := v.(bool); ok && rawOutput {
|
||||
if row := r.parseLogRow(entry); row != nil {
|
||||
if step.LogLength == 0 {
|
||||
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
||||
}
|
||||
step.LogLength++
|
||||
r.appendLogRow(row)
|
||||
if step.LogLength == 0 {
|
||||
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
||||
}
|
||||
step.LogLength++
|
||||
r.appendLogRow(logRow(entry))
|
||||
}
|
||||
} else if r.shouldAppendLogRow(entry) {
|
||||
r.appendLogRow(r.parseLogRow(entry))
|
||||
r.appendLogRow(logRow(entry))
|
||||
}
|
||||
if v, ok := entry.Attr("stepResult"); ok && isJobStepEntry(entry) {
|
||||
if stepResult, ok := r.parseResult(v); ok {
|
||||
@@ -654,8 +638,8 @@ func (r *Reporter) Logf(format string, a ...any) {
|
||||
|
||||
func (r *Reporter) logf(format string, a ...any) {
|
||||
if !r.duringSteps() {
|
||||
// Masked like any other row: these bypass parseLogRow, but a caller can still
|
||||
// interpolate a secret, such as a configured URL carrying credentials.
|
||||
// Masked like any other row, as a caller can interpolate a secret such as a configured URL
|
||||
// carrying credentials.
|
||||
r.appendLogRow(&runnerv1.LogRow{Time: timestamppb.Now(), Content: fmt.Sprintf(format, a...)})
|
||||
}
|
||||
}
|
||||
@@ -1000,129 +984,8 @@ func (r *Reporter) parseResult(result any) (runnerv1.Result, bool) {
|
||||
return ret, ok
|
||||
}
|
||||
|
||||
// A property value never contains a raw ':' (GitHub escapes it as %3A), so excluding ':' ends
|
||||
// the property list at the first '::' as GitHub does; greedily would swallow a '::' message.
|
||||
var cmdRegex = regexp.MustCompile(`^::([^ :]+)( [^:]*)?::(.*)$`)
|
||||
|
||||
// handleCommand takes value still escaped, so that the web UI decodes it exactly once. Only
|
||||
// the branches that consume the payload here decode it.
|
||||
func (r *Reporter) handleCommand(originalContent, command, properties, value string) *string {
|
||||
command = strings.ToLower(command) // GitHub matches command names case-insensitively
|
||||
if r.stopCommandEndToken != "" {
|
||||
if !strings.EqualFold(command, r.stopCommandEndToken) {
|
||||
return &originalContent
|
||||
}
|
||||
// Resumed here rather than from the switch, because the end token is arbitrary and a
|
||||
// token naming a real command would otherwise never resume.
|
||||
r.stopCommandEndToken = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
switch command {
|
||||
case "add-mask":
|
||||
r.addMask(runner.UnescapeCommandData(value))
|
||||
return nil
|
||||
case "debug":
|
||||
if r.debugOutputEnabled {
|
||||
return &originalContent // kept as ::debug::, so the web UI labels and decodes it
|
||||
}
|
||||
return nil
|
||||
|
||||
case "notice", "warning", "error":
|
||||
// Gitea has no annotation store, so the annotation is rendered into the log with
|
||||
// its source location instead of being dropped: that location is the whole point
|
||||
// of the command for compiler and linter output.
|
||||
annotation := formatAnnotation(command, properties, value)
|
||||
return &annotation
|
||||
case "group", "endgroup":
|
||||
// Passed through: the web UI folds the log on these and decodes the payload itself.
|
||||
return &originalContent
|
||||
case "stop-commands":
|
||||
r.stopCommandEndToken = runner.UnescapeCommandData(value)
|
||||
return nil
|
||||
}
|
||||
return &originalContent
|
||||
}
|
||||
|
||||
// formatAnnotation folds the file, line, column and title the command carries into its message,
|
||||
// which the web UI otherwise drops along with the rest of the properties:
|
||||
//
|
||||
// ::error file=main.go,line=12,col=5,title=vet::undefined: x
|
||||
// ::error::main.go:12:5: vet: undefined: x
|
||||
//
|
||||
// The ::-form prefix is deliberate, and value is not escaped here because it arrived escaped
|
||||
// and must stay that way.
|
||||
func formatAnnotation(level, properties, value string) string {
|
||||
props := parseCommandProperties(properties)
|
||||
|
||||
prefix := props["file"]
|
||||
if prefix != "" {
|
||||
if props["line"] != "" {
|
||||
prefix += ":" + props["line"]
|
||||
if props["col"] != "" {
|
||||
prefix += ":" + props["col"]
|
||||
}
|
||||
}
|
||||
prefix += ": "
|
||||
}
|
||||
if props["title"] != "" {
|
||||
prefix += props["title"] + ": "
|
||||
}
|
||||
return "::" + level + "::" + prefix + value
|
||||
}
|
||||
|
||||
// parseCommandProperties parses the `file=main.go,line=12` part of a workflow command.
|
||||
func parseCommandProperties(properties string) map[string]string {
|
||||
properties = strings.TrimSpace(properties)
|
||||
if properties == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
props := map[string]string{}
|
||||
for pair := range strings.SplitSeq(properties, ",") {
|
||||
key, value, ok := strings.Cut(pair, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Only the property-list separators are decoded, the web UI decodes the rest.
|
||||
value = strings.ReplaceAll(strings.ReplaceAll(value, "%3A", ":"), "%2C", ",")
|
||||
// GitHub keys its property dictionary case-insensitively, so `File=` works there too.
|
||||
props[strings.ToLower(strings.TrimSpace(key))] = value
|
||||
}
|
||||
// GitHub's toolkit emits `col`; accept `column` as well, which some tools write instead.
|
||||
if props["col"] == "" {
|
||||
props["col"] = props["column"]
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
func cutPrefixFold(s, prefix string) (string, bool) {
|
||||
if len(s) < len(prefix) || !strings.EqualFold(s[:len(prefix)], prefix) {
|
||||
return s, false
|
||||
}
|
||||
return s[len(prefix):], true
|
||||
}
|
||||
|
||||
func (r *Reporter) parseLogRow(entry logging.Record) *runnerv1.LogRow {
|
||||
content := strings.TrimRight(entry.Message, "\r\n")
|
||||
|
||||
// cmdRegex only covers the ::cmd:: form, so the ##[add-mask] one would otherwise reach
|
||||
// the log carrying its own secret. Registered and dropped like its ::add-mask:: twin.
|
||||
if arg, ok := cutPrefixFold(content, "##[add-mask]"); ok {
|
||||
r.addMask(runner.UnescapeLegacyCommand(arg))
|
||||
return nil
|
||||
}
|
||||
|
||||
matches := cmdRegex.FindStringSubmatch(content)
|
||||
if matches != nil {
|
||||
if output := r.handleCommand(content, matches[1], matches[2], matches[3]); output != nil {
|
||||
content = *output
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return &runnerv1.LogRow{Time: timestamppb.New(entry.Time), Content: content}
|
||||
func logRow(entry logging.Record) *runnerv1.LogRow {
|
||||
return &runnerv1.LogRow{Time: timestamppb.New(entry.Time), Content: strings.TrimRight(entry.Message, "\r\n")}
|
||||
}
|
||||
|
||||
// mask repairs the content first, so a secret the repair itself spells out is still caught.
|
||||
@@ -1130,10 +993,12 @@ func (r *Reporter) mask(content string) string {
|
||||
return r.logReplacer.Replace(strings.ToValidUTF8(content, "?"))
|
||||
}
|
||||
|
||||
// addMask deliberately leaves the process-wide masker alone. Its entries come from every live
|
||||
// AddMask deliberately leaves the process-wide masker alone. Its entries come from every live
|
||||
// task at once, so a workflow could otherwise mask "error" there and rewrite the runner's own
|
||||
// log, and every other task's, for the rest of the job.
|
||||
func (r *Reporter) addMask(msg string) {
|
||||
func (r *Reporter) AddMask(msg string) {
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
r.oldnew = runner.AppendSecretMasker(r.oldnew, msg)
|
||||
r.logReplacer = runner.NewSecretReplacer(r.oldnew)
|
||||
}
|
||||
|
||||
@@ -35,259 +35,19 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestReporter_parseLogRow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
debugOutputEnabled bool
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
"No command", false,
|
||||
[]string{"Hello, world!"},
|
||||
[]string{"Hello, world!"},
|
||||
},
|
||||
{
|
||||
"Add-mask", false,
|
||||
[]string{
|
||||
"foo mysecret bar",
|
||||
"::add-mask::mysecret",
|
||||
"foo mysecret bar",
|
||||
},
|
||||
[]string{
|
||||
"foo mysecret bar",
|
||||
"<nil>",
|
||||
"foo *** bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Add-mask-multiline", false,
|
||||
[]string{
|
||||
"foo mysecret bar",
|
||||
"::add-mask::LINE1%0ALINE2",
|
||||
"foo LINE1 bar",
|
||||
},
|
||||
[]string{
|
||||
"foo mysecret bar",
|
||||
"<nil>",
|
||||
"foo *** bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Debug enabled", true,
|
||||
[]string{
|
||||
"::debug::GitHub Actions runtime token access controls",
|
||||
// Left escaped: the web UI decodes it, and a real newline would not survive storage.
|
||||
"::debug::first%0Asecond",
|
||||
},
|
||||
[]string{
|
||||
"::debug::GitHub Actions runtime token access controls",
|
||||
"::debug::first%0Asecond",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Debug not enabled", false,
|
||||
[]string{
|
||||
"::debug::GitHub Actions runtime token access controls",
|
||||
},
|
||||
[]string{
|
||||
"<nil>",
|
||||
},
|
||||
},
|
||||
// The three annotation levels share one code path, so the property shapes are only
|
||||
// exercised under "error"; notice and warning just prove the level token round-trips.
|
||||
{
|
||||
"notice", false,
|
||||
[]string{
|
||||
"::notice::Gosh, that's not going to work",
|
||||
},
|
||||
[]string{
|
||||
"::notice::Gosh, that's not going to work",
|
||||
},
|
||||
},
|
||||
{
|
||||
"warning", false,
|
||||
[]string{
|
||||
"::warning::Gosh, that's not going to work",
|
||||
},
|
||||
[]string{
|
||||
"::warning::Gosh, that's not going to work",
|
||||
},
|
||||
},
|
||||
{
|
||||
"error", false,
|
||||
[]string{
|
||||
"::error file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||
"::error::Gosh, that's not going to work",
|
||||
"::error file=file.name,line=42,col=7::Gosh, that's not going to work",
|
||||
// The message keeps its own '::', the property list ends at the first one.
|
||||
"::error file=main.cpp,line=12::no member named 'foo' in 'std::vector<int>'",
|
||||
// GitHub matches property names case-insensitively.
|
||||
"::error File=file.name,Line=42,Col=7::Gosh, that's not going to work",
|
||||
// Only the property separators are decoded here, %25/%0A are left for the web UI.
|
||||
"::error file=a%3Ab.go,title=100%252C::still %25 escaped%0Aand multi-line",
|
||||
},
|
||||
[]string{
|
||||
"::error::file.name:42: Cool Title: Gosh, that's not going to work",
|
||||
"::error::Gosh, that's not going to work",
|
||||
"::error::file.name:42:7: Gosh, that's not going to work",
|
||||
"::error::main.cpp:12: no member named 'foo' in 'std::vector<int>'",
|
||||
"::error::file.name:42:7: Gosh, that's not going to work",
|
||||
"::error::a:b.go: 100%252C: still %25 escaped%0Aand multi-line",
|
||||
},
|
||||
},
|
||||
{
|
||||
"group", false,
|
||||
[]string{
|
||||
"::group::",
|
||||
"::endgroup::",
|
||||
},
|
||||
[]string{
|
||||
"::group::",
|
||||
"::endgroup::",
|
||||
},
|
||||
},
|
||||
{
|
||||
"stop-commands", false,
|
||||
[]string{
|
||||
"::add-mask::foo",
|
||||
"::stop-commands::myverycoolstoptoken",
|
||||
"::add-mask::bar",
|
||||
"::debug::Stuff",
|
||||
"myverycoolstoptoken",
|
||||
"::add-mask::baz",
|
||||
"::myverycoolstoptoken::",
|
||||
"::add-mask::wibble",
|
||||
"foo bar baz wibble",
|
||||
},
|
||||
[]string{
|
||||
"<nil>",
|
||||
"<nil>",
|
||||
"::add-mask::bar",
|
||||
"::debug::Stuff",
|
||||
"myverycoolstoptoken",
|
||||
"::add-mask::baz",
|
||||
"<nil>",
|
||||
"<nil>",
|
||||
"*** bar baz ***",
|
||||
},
|
||||
},
|
||||
{
|
||||
// a token naming a real command must still resume
|
||||
"stop-commands with a command-named token", false,
|
||||
[]string{
|
||||
"::stop-commands::add-mask",
|
||||
"::set-output name=x::suppressed",
|
||||
"::add-mask::",
|
||||
"::add-mask::masked",
|
||||
"masked",
|
||||
},
|
||||
[]string{
|
||||
"<nil>",
|
||||
"::set-output name=x::suppressed",
|
||||
"<nil>",
|
||||
"<nil>",
|
||||
"***",
|
||||
},
|
||||
},
|
||||
{
|
||||
"unknown command", false,
|
||||
[]string{
|
||||
"::set-mask::foo",
|
||||
},
|
||||
[]string{
|
||||
"::set-mask::foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Reporter{
|
||||
logReplacer: strings.NewReplacer(),
|
||||
debugOutputEnabled: tt.debugOutputEnabled,
|
||||
}
|
||||
for idx, arg := range tt.args {
|
||||
rv := r.parseLogRow(logging.Record{Message: arg})
|
||||
got := "<nil>"
|
||||
func TestReporter_CompositeStepResultDoesNotFinishStep(t *testing.T) {
|
||||
reporter := &Reporter{state: &runnerv1.TaskState{}, logReplacer: strings.NewReplacer()}
|
||||
reporter.ResetSteps(newTestSteps(1))
|
||||
data := map[string]any{"stage": runner.StageMain, "stepNumber": 0, "stepID": []string{"0", "0"}, "raw_output": true, "stepResult": model.StepStatusFailure}
|
||||
|
||||
if rv != nil {
|
||||
got = r.mask(rv.Content)
|
||||
}
|
||||
require.NoError(t, fire(reporter, logEntry{Data: data}))
|
||||
assert.Equal(t, runnerv1.Result_RESULT_UNSPECIFIED, reporter.state.Steps[0].Result)
|
||||
|
||||
assert.Equal(t, tt.want[idx], got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Both add-mask forms must register the secret and drop their own row: the runner forwards
|
||||
// the raw line, so failing to consume it writes the secret straight to the job log.
|
||||
func TestReporter_parseLogRowAddMask(t *testing.T) {
|
||||
for _, line := range []string{"::add-mask::supersecret", "##[add-mask]supersecret"} {
|
||||
r := &Reporter{logReplacer: strings.NewReplacer()}
|
||||
|
||||
assert.Nil(t, r.parseLogRow(logging.Record{Message: line}), line)
|
||||
|
||||
assert.Equal(t, "using *** now", r.mask("using supersecret now"), line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReporter_Handle(t *testing.T) {
|
||||
t.Run("ignore command lines", func(t *testing.T) {
|
||||
client := mocks.NewClient(t)
|
||||
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
||||
t.Logf("Received UpdateLog: %s", req.Msg.String())
|
||||
return connect_go.NewResponse(&runnerv1.UpdateLogResponse{
|
||||
AckIndex: req.Msg.Index + int64(len(req.Msg.Rows)),
|
||||
}), nil
|
||||
})
|
||||
client.On("UpdateTask", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
|
||||
t.Logf("Received UpdateTask: %s", req.Msg.String())
|
||||
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{}), nil
|
||||
})
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||
require.NoError(t, err)
|
||||
cfg, _ := config.LoadDefault("")
|
||||
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{
|
||||
Context: taskCtx,
|
||||
}, cfg)
|
||||
reporter.RunDaemon()
|
||||
defer func() {
|
||||
require.NoError(t, reporter.Close(""))
|
||||
}()
|
||||
reporter.ResetSteps(newTestSteps(5))
|
||||
|
||||
dataStep0 := map[string]any{
|
||||
"stage": "Main",
|
||||
"stepNumber": 0,
|
||||
"raw_output": true,
|
||||
}
|
||||
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "regular log line", Data: dataStep0}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "regular log line", Data: dataStep0}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "regular log line", Data: dataStep0}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "composite step result", Data: map[string]any{
|
||||
"stage": "Main",
|
||||
"stepID": []string{"0", "0"},
|
||||
"stepNumber": 0,
|
||||
"raw_output": true,
|
||||
"stepResult": model.StepStatusFailure,
|
||||
}}))
|
||||
assert.Equal(t, runnerv1.Result_RESULT_UNSPECIFIED, reporter.state.Steps[0].Result)
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "step result", Data: map[string]any{
|
||||
"stage": "Main",
|
||||
"stepNumber": 0,
|
||||
"raw_output": true,
|
||||
"stepResult": model.StepStatusSuccess,
|
||||
}}))
|
||||
assert.Equal(t, runnerv1.Result_RESULT_SUCCESS, reporter.state.Steps[0].Result)
|
||||
|
||||
assert.Equal(t, int64(5), reporter.state.Steps[0].LogLength)
|
||||
})
|
||||
delete(data, "stepID")
|
||||
data["stepResult"] = model.StepStatusSuccess
|
||||
require.NoError(t, fire(reporter, logEntry{Data: data}))
|
||||
assert.Equal(t, runnerv1.Result_RESULT_SUCCESS, reporter.state.Steps[0].Result)
|
||||
assert.Equal(t, int64(2), reporter.state.Steps[0].LogLength)
|
||||
}
|
||||
|
||||
func TestReporter_LogLevelFiltering(t *testing.T) {
|
||||
@@ -1047,7 +807,6 @@ func TestReporter_StopHeartbeats(t *testing.T) {
|
||||
|
||||
func TestAppendLogRow(t *testing.T) {
|
||||
r := &Reporter{logReplacer: strings.NewReplacer("supersecret", "***")}
|
||||
r.appendLogRow(nil)
|
||||
r.appendLogRow(&runnerv1.LogRow{Time: timestamppb.Now(), Content: "hello supersecret"})
|
||||
require.Len(t, r.logRows, 1)
|
||||
assert.Equal(t, "hello ***", r.logRows[0].Content)
|
||||
@@ -1056,6 +815,8 @@ func TestAppendLogRow(t *testing.T) {
|
||||
r = &Reporter{logReplacer: strings.NewReplacer("a?b", "***")}
|
||||
r.appendLogRow(&runnerv1.LogRow{Time: timestamppb.Now(), Content: "a\xffb"})
|
||||
assert.Equal(t, "***", r.logRows[0].Content)
|
||||
|
||||
assert.Equal(t, "::debug::d%0Anext", logRow(logging.Record{Message: "::debug::d%0Anext\r\n"}).Content)
|
||||
}
|
||||
|
||||
func TestReporter_Enabled(t *testing.T) {
|
||||
@@ -1122,7 +883,6 @@ func TestReporter_DebugSettingsAreNotMasked(t *testing.T) {
|
||||
}, &config.Config{})
|
||||
defer deregisterGlobalMasks(reporter)
|
||||
|
||||
assert.True(t, reporter.debugOutputEnabled)
|
||||
assert.Equal(t, "debug is true", reporter.mask("debug is true"))
|
||||
assert.Equal(t, "***", reporter.mask("s3cr3t"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user