refactor: extract setupAction into ActionReader (#986)
This change extracts the functionality of reading an `action.y(a)ml` or creation of a `(Synthetic Action)` into its own type to enable better unit testing / mocking of those IO operations. This is done in preparation for the implementation of pre/post action support in act. Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Markus Wolf <markus.wolf@new-work.se>
This commit is contained in:
parent
e23223ad02
commit
e4f0080a1e
3 changed files with 246 additions and 85 deletions
84
pkg/runner/action.go
Normal file
84
pkg/runner/action.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActionReader interface {
|
||||||
|
readAction(step *model.Step, actionDir string, actionPath string, readFile actionyamlReader) (*model.Action, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionyamlReader func(filename string) (io.Reader, io.Closer, error)
|
||||||
|
type fileWriter func(filename string, data []byte, perm fs.FileMode) error
|
||||||
|
|
||||||
|
//go:embed res/trampoline.js
|
||||||
|
var trampoline embed.FS
|
||||||
|
|
||||||
|
func (sc *StepContext) readAction(step *model.Step, actionDir string, actionPath string, readFile actionyamlReader, writeFile fileWriter) (*model.Action, error) {
|
||||||
|
reader, closer, err := readFile("action.yml")
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
reader, closer, err = readFile("action.yaml")
|
||||||
|
if err != nil {
|
||||||
|
if _, closer, err2 := readFile("Dockerfile"); err2 == nil {
|
||||||
|
closer.Close()
|
||||||
|
action := &model.Action{
|
||||||
|
Name: "(Synthetic)",
|
||||||
|
Runs: model.ActionRuns{
|
||||||
|
Using: "docker",
|
||||||
|
Image: "Dockerfile",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
log.Debugf("Using synthetic action %v for Dockerfile", action)
|
||||||
|
return action, nil
|
||||||
|
}
|
||||||
|
if step.With != nil {
|
||||||
|
if val, ok := step.With["args"]; ok {
|
||||||
|
var b []byte
|
||||||
|
if b, err = trampoline.ReadFile("res/trampoline.js"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err2 := writeFile(filepath.Join(actionDir, actionPath, "trampoline.js"), b, 0400)
|
||||||
|
if err2 != nil {
|
||||||
|
return nil, err2
|
||||||
|
}
|
||||||
|
action := &model.Action{
|
||||||
|
Name: "(Synthetic)",
|
||||||
|
Inputs: map[string]model.Input{
|
||||||
|
"cwd": {
|
||||||
|
Description: "(Actual working directory)",
|
||||||
|
Required: false,
|
||||||
|
Default: filepath.Join(actionDir, actionPath),
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
Description: "(Actual program)",
|
||||||
|
Required: false,
|
||||||
|
Default: val,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Runs: model.ActionRuns{
|
||||||
|
Using: "node12",
|
||||||
|
Main: "trampoline.js",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
log.Debugf("Using synthetic action %v", action)
|
||||||
|
return action, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer closer.Close()
|
||||||
|
|
||||||
|
action, err := model.ReadAction(reader)
|
||||||
|
log.Debugf("Read action %v from '%s'", action, "Unknown")
|
||||||
|
return action, err
|
||||||
|
}
|
133
pkg/runner/action_test.go
Normal file
133
pkg/runner/action_test.go
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type closerMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *closerMock) Close() error {
|
||||||
|
m.Called()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionReader(t *testing.T) {
|
||||||
|
yaml := strings.ReplaceAll(`
|
||||||
|
name: 'name'
|
||||||
|
runs:
|
||||||
|
using: 'node16'
|
||||||
|
main: 'main.js'
|
||||||
|
`, "\t", " ")
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
step *model.Step
|
||||||
|
filename string
|
||||||
|
fileContent string
|
||||||
|
expected *model.Action
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "readActionYml",
|
||||||
|
step: &model.Step{},
|
||||||
|
filename: "action.yml",
|
||||||
|
fileContent: yaml,
|
||||||
|
expected: &model.Action{
|
||||||
|
Name: "name",
|
||||||
|
Runs: model.ActionRuns{
|
||||||
|
Using: "node16",
|
||||||
|
Main: "main.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "readActionYaml",
|
||||||
|
step: &model.Step{},
|
||||||
|
filename: "action.yaml",
|
||||||
|
fileContent: yaml,
|
||||||
|
expected: &model.Action{
|
||||||
|
Name: "name",
|
||||||
|
Runs: model.ActionRuns{
|
||||||
|
Using: "node16",
|
||||||
|
Main: "main.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "readDockerfile",
|
||||||
|
step: &model.Step{},
|
||||||
|
filename: "Dockerfile",
|
||||||
|
fileContent: "FROM ubuntu:20.04",
|
||||||
|
expected: &model.Action{
|
||||||
|
Name: "(Synthetic)",
|
||||||
|
Runs: model.ActionRuns{
|
||||||
|
Using: "docker",
|
||||||
|
Image: "Dockerfile",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "readWithArgs",
|
||||||
|
step: &model.Step{
|
||||||
|
With: map[string]string{
|
||||||
|
"args": "cmd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &model.Action{
|
||||||
|
Name: "(Synthetic)",
|
||||||
|
Inputs: map[string]model.Input{
|
||||||
|
"cwd": {
|
||||||
|
Description: "(Actual working directory)",
|
||||||
|
Required: false,
|
||||||
|
Default: "actionDir/actionPath",
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
Description: "(Actual program)",
|
||||||
|
Required: false,
|
||||||
|
Default: "cmd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Runs: model.ActionRuns{
|
||||||
|
Using: "node12",
|
||||||
|
Main: "trampoline.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range table {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
closerMock := &closerMock{}
|
||||||
|
|
||||||
|
readFile := func(filename string) (io.Reader, io.Closer, error) {
|
||||||
|
if tt.filename != filename {
|
||||||
|
return nil, nil, fs.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.NewReader(tt.fileContent), closerMock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile := func(filename string, data []byte, perm fs.FileMode) error {
|
||||||
|
assert.Equal(t, "actionDir/actionPath/trampoline.js", filename)
|
||||||
|
assert.Equal(t, fs.FileMode(0400), perm)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
closerMock.On("Close")
|
||||||
|
|
||||||
|
sc := &StepContext{}
|
||||||
|
action, err := sc.readAction(tt.step, "actionDir", "actionPath", readFile, writeFile)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, tt.expected, action)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package runner
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -64,8 +63,24 @@ func (sc *StepContext) Executor(ctx context.Context) common.Executor {
|
||||||
|
|
||||||
case model.StepTypeUsesActionLocal:
|
case model.StepTypeUsesActionLocal:
|
||||||
actionDir := filepath.Join(rc.Config.Workdir, step.Uses)
|
actionDir := filepath.Join(rc.Config.Workdir, step.Uses)
|
||||||
|
|
||||||
|
localReader := func(ctx context.Context) actionyamlReader {
|
||||||
|
_, cpath := sc.getContainerActionPaths(sc.Step, path.Join(actionDir, ""), sc.RunContext)
|
||||||
|
return func(filename string) (io.Reader, io.Closer, error) {
|
||||||
|
tars, err := sc.RunContext.JobContainer.GetContainerArchive(ctx, path.Join(cpath, filename))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
treader := tar.NewReader(tars)
|
||||||
|
if _, err := treader.Next(); err != nil {
|
||||||
|
return nil, nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
return treader, tars, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
sc.setupAction(actionDir, "", true),
|
sc.setupAction(actionDir, "", localReader),
|
||||||
sc.runAction(actionDir, "", "", "", true),
|
sc.runAction(actionDir, "", "", "", true),
|
||||||
)
|
)
|
||||||
case model.StepTypeUsesActionRemote:
|
case model.StepTypeUsesActionRemote:
|
||||||
|
@ -102,9 +117,17 @@ func (sc *StepContext) Executor(ctx context.Context) common.Executor {
|
||||||
ntErr = common.NewInfoExecutor("Non-terminating error while running 'git clone': %v", err)
|
ntErr = common.NewInfoExecutor("Non-terminating error while running 'git clone': %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remoteReader := func(ctx context.Context) actionyamlReader {
|
||||||
|
return func(filename string) (io.Reader, io.Closer, error) {
|
||||||
|
f, err := os.Open(filepath.Join(actionDir, remoteAction.Path, filename))
|
||||||
|
return f, f, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
ntErr,
|
ntErr,
|
||||||
sc.setupAction(actionDir, remoteAction.Path, false),
|
sc.setupAction(actionDir, remoteAction.Path, remoteReader),
|
||||||
sc.runAction(actionDir, remoteAction.Path, remoteAction.Repo, remoteAction.Ref, false),
|
sc.runAction(actionDir, remoteAction.Path, remoteAction.Repo, remoteAction.Ref, false),
|
||||||
)
|
)
|
||||||
case model.StepTypeInvalid:
|
case model.StepTypeInvalid:
|
||||||
|
@ -365,89 +388,10 @@ func (sc *StepContext) runUsesContainer() common.Executor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed res/trampoline.js
|
func (sc *StepContext) setupAction(actionDir string, actionPath string, reader func(context.Context) actionyamlReader) common.Executor {
|
||||||
var trampoline embed.FS
|
|
||||||
|
|
||||||
func (sc *StepContext) setupAction(actionDir string, actionPath string, localAction bool) common.Executor {
|
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
var readFile func(filename string) (io.Reader, io.Closer, error)
|
action, err := sc.readAction(sc.Step, actionDir, actionPath, reader(ctx), ioutil.WriteFile)
|
||||||
if localAction {
|
sc.Action = action
|
||||||
_, cpath := sc.getContainerActionPaths(sc.Step, path.Join(actionDir, actionPath), sc.RunContext)
|
|
||||||
readFile = func(filename string) (io.Reader, io.Closer, error) {
|
|
||||||
tars, err := sc.RunContext.JobContainer.GetContainerArchive(ctx, path.Join(cpath, filename))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, os.ErrNotExist
|
|
||||||
}
|
|
||||||
treader := tar.NewReader(tars)
|
|
||||||
if _, err := treader.Next(); err != nil {
|
|
||||||
return nil, nil, os.ErrNotExist
|
|
||||||
}
|
|
||||||
return treader, tars, nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
readFile = func(filename string) (io.Reader, io.Closer, error) {
|
|
||||||
f, err := os.Open(filepath.Join(actionDir, actionPath, filename))
|
|
||||||
return f, f, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, closer, err := readFile("action.yml")
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
reader, closer, err = readFile("action.yaml")
|
|
||||||
if err != nil {
|
|
||||||
if _, closer, err2 := readFile("Dockerfile"); err2 == nil {
|
|
||||||
closer.Close()
|
|
||||||
sc.Action = &model.Action{
|
|
||||||
Name: "(Synthetic)",
|
|
||||||
Runs: model.ActionRuns{
|
|
||||||
Using: "docker",
|
|
||||||
Image: "Dockerfile",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
log.Debugf("Using synthetic action %v for Dockerfile", sc.Action)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if sc.Step.With != nil {
|
|
||||||
if val, ok := sc.Step.With["args"]; ok {
|
|
||||||
var b []byte
|
|
||||||
if b, err = trampoline.ReadFile("res/trampoline.js"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err2 := ioutil.WriteFile(filepath.Join(actionDir, actionPath, "trampoline.js"), b, 0400)
|
|
||||||
if err2 != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sc.Action = &model.Action{
|
|
||||||
Name: "(Synthetic)",
|
|
||||||
Inputs: map[string]model.Input{
|
|
||||||
"cwd": {
|
|
||||||
Description: "(Actual working directory)",
|
|
||||||
Required: false,
|
|
||||||
Default: filepath.Join(actionDir, actionPath),
|
|
||||||
},
|
|
||||||
"command": {
|
|
||||||
Description: "(Actual program)",
|
|
||||||
Required: false,
|
|
||||||
Default: val,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Runs: model.ActionRuns{
|
|
||||||
Using: "node12",
|
|
||||||
Main: "trampoline.js",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
log.Debugf("Using synthetic action %v", sc.Action)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer closer.Close()
|
|
||||||
|
|
||||||
sc.Action, err = model.ReadAction(reader)
|
|
||||||
log.Debugf("Read action %v from '%s'", sc.Action, "Unknown")
|
log.Debugf("Read action %v from '%s'", sc.Action, "Unknown")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue