Workflow commands (#149)
Establishes a simple framework for supporting workflow commands. Fully implements `::add-mask::`, `::debug::`, and `::stop-commands::`. Addresses #148 Co-authored-by: Jason Song <i@wolfogre.com> Reviewed-on: https://gitea.com/gitea/act_runner/pulls/149 Reviewed-by: Jason Song <i@wolfogre.com> Co-authored-by: Søren L. Hansen <sorenisanerd@gmail.com> Co-committed-by: Søren L. Hansen <sorenisanerd@gmail.com>
This commit is contained in:
parent
3be962cdb3
commit
c8cc7b2448
|
@ -6,6 +6,7 @@ package report
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -31,10 +32,14 @@ type Reporter struct {
|
||||||
logOffset int
|
logOffset int
|
||||||
logRows []*runnerv1.LogRow
|
logRows []*runnerv1.LogRow
|
||||||
logReplacer *strings.Replacer
|
logReplacer *strings.Replacer
|
||||||
|
oldnew []string
|
||||||
|
|
||||||
state *runnerv1.TaskState
|
state *runnerv1.TaskState
|
||||||
stateMu sync.RWMutex
|
stateMu sync.RWMutex
|
||||||
outputs sync.Map
|
outputs sync.Map
|
||||||
|
|
||||||
|
debugOutputEnabled bool
|
||||||
|
stopCommandEndToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
|
func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.Client, task *runnerv1.Task) *Reporter {
|
||||||
|
@ -46,15 +51,22 @@ func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.C
|
||||||
oldnew = append(oldnew, v, "***")
|
oldnew = append(oldnew, v, "***")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Reporter{
|
rv := &Reporter{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
client: client,
|
client: client,
|
||||||
|
oldnew: oldnew,
|
||||||
logReplacer: strings.NewReplacer(oldnew...),
|
logReplacer: strings.NewReplacer(oldnew...),
|
||||||
state: &runnerv1.TaskState{
|
state: &runnerv1.TaskState{
|
||||||
Id: task.Id,
|
Id: task.Id,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if task.Secrets["ACTIONS_STEP_DEBUG"] == "true" {
|
||||||
|
rv.debugOutputEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return rv
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) ResetSteps(l int) {
|
func (r *Reporter) ResetSteps(l int) {
|
||||||
|
@ -71,6 +83,13 @@ func (r *Reporter) Levels() []log.Level {
|
||||||
return log.AllLevels
|
return log.AllLevels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appendIfNotNil[T any](s []*T, v *T) []*T {
|
||||||
|
if v != nil {
|
||||||
|
return append(s, v)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Reporter) Fire(entry *log.Entry) error {
|
func (r *Reporter) Fire(entry *log.Entry) error {
|
||||||
r.stateMu.Lock()
|
r.stateMu.Lock()
|
||||||
defer r.stateMu.Unlock()
|
defer r.stateMu.Unlock()
|
||||||
|
@ -97,7 +116,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !r.duringSteps() {
|
if !r.duringSteps() {
|
||||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -110,7 +129,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
||||||
}
|
}
|
||||||
if step == nil {
|
if step == nil {
|
||||||
if !r.duringSteps() {
|
if !r.duringSteps() {
|
||||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -124,10 +143,10 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
||||||
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
||||||
}
|
}
|
||||||
step.LogLength++
|
step.LogLength++
|
||||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||||
}
|
}
|
||||||
} else if !r.duringSteps() {
|
} else if !r.duringSteps() {
|
||||||
r.logRows = append(r.logRows, r.parseLogRow(entry))
|
r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry))
|
||||||
}
|
}
|
||||||
if v, ok := entry.Data["stepResult"]; ok {
|
if v, ok := entry.Data["stepResult"]; ok {
|
||||||
if stepResult, ok := r.parseResult(v); ok {
|
if stepResult, ok := r.parseResult(v); ok {
|
||||||
|
@ -338,11 +357,70 @@ func (r *Reporter) parseResult(result interface{}) (runnerv1.Result, bool) {
|
||||||
return ret, ok
|
return ret, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cmdRegex = regexp.MustCompile(`^::([^ :]+)( .*)?::(.*)$`)
|
||||||
|
|
||||||
|
func (r *Reporter) handleCommand(originalContent, command, parameters, value string) *string {
|
||||||
|
if r.stopCommandEndToken != "" && command != r.stopCommandEndToken {
|
||||||
|
return &originalContent
|
||||||
|
}
|
||||||
|
|
||||||
|
switch command {
|
||||||
|
case "add-mask":
|
||||||
|
r.addMask(value)
|
||||||
|
return nil
|
||||||
|
case "debug":
|
||||||
|
if r.debugOutputEnabled {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case "notice":
|
||||||
|
// Not implemented yet, so just return the original content.
|
||||||
|
return &originalContent
|
||||||
|
case "warning":
|
||||||
|
// Not implemented yet, so just return the original content.
|
||||||
|
return &originalContent
|
||||||
|
case "error":
|
||||||
|
// Not implemented yet, so just return the original content.
|
||||||
|
return &originalContent
|
||||||
|
case "group":
|
||||||
|
// Returning the original content, because I think the frontend
|
||||||
|
// will use it when rendering the output.
|
||||||
|
return &originalContent
|
||||||
|
case "endgroup":
|
||||||
|
// Ditto
|
||||||
|
return &originalContent
|
||||||
|
case "stop-commands":
|
||||||
|
r.stopCommandEndToken = value
|
||||||
|
return nil
|
||||||
|
case r.stopCommandEndToken:
|
||||||
|
r.stopCommandEndToken = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &originalContent
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
|
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
|
||||||
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
|
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
content = r.logReplacer.Replace(content)
|
content = r.logReplacer.Replace(content)
|
||||||
|
|
||||||
return &runnerv1.LogRow{
|
return &runnerv1.LogRow{
|
||||||
Time: timestamppb.New(entry.Time),
|
Time: timestamppb.New(entry.Time),
|
||||||
Content: content,
|
Content: content,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Reporter) addMask(msg string) {
|
||||||
|
r.oldnew = append(r.oldnew, msg, "***")
|
||||||
|
r.logReplacer = strings.NewReplacer(r.oldnew...)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package report
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Debug enabled", true,
|
||||||
|
[]string{
|
||||||
|
"::debug::GitHub Actions runtime token access controls",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"GitHub Actions runtime token access controls",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Debug not enabled", false,
|
||||||
|
[]string{
|
||||||
|
"::debug::GitHub Actions runtime token access controls",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"<nil>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"notice", false,
|
||||||
|
[]string{
|
||||||
|
"::notice file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"::notice file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"warning", false,
|
||||||
|
[]string{
|
||||||
|
"::warning file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"::warning file=file.name,line=42,endLine=48,title=Cool Title::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",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"::error file=file.name,line=42,endLine=48,title=Cool Title::Gosh, that's not going to work",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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 ***",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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(&log.Entry{Message: arg})
|
||||||
|
got := "<nil>"
|
||||||
|
|
||||||
|
if rv != nil {
|
||||||
|
got = rv.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tt.want[idx], got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue