mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2025-08-06 17:40:58 +00:00
successfully able to run simple workflows
Signed-off-by: Casey Lee <cplee@nektos.com>
This commit is contained in:
parent
fbab49c68d
commit
33f8290eb3
21 changed files with 951 additions and 468 deletions
12
.github/workflows/basic.yml
vendored
12
.github/workflows/basic.yml
vendored
|
@ -4,5 +4,15 @@ on: push
|
|||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:10.16-jessie
|
||||
env:
|
||||
NODE_ENV: development
|
||||
steps:
|
||||
- run: echo hello world!
|
||||
- run: env
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: cp $GITHUB_EVENT_PATH $HOME/foo.json
|
||||
- run: ls $HOME
|
||||
- run: cat $HOME/foo.json
|
||||
|
|
25
act/common/dryrun.go
Normal file
25
act/common/dryrun.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type dryrunContextKey string
|
||||
|
||||
const dryrunContextKeyVal = dryrunContextKey("dryrun")
|
||||
|
||||
// Dryrun returns true if the current context is dryrun
|
||||
func Dryrun(ctx context.Context) bool {
|
||||
val := ctx.Value(dryrunContextKeyVal)
|
||||
if val != nil {
|
||||
if dryrun, ok := val.(bool); ok {
|
||||
return dryrun
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WithDryrun adds a value to the context for dryrun
|
||||
func WithDryrun(ctx context.Context, dryrun bool) context.Context {
|
||||
return context.WithValue(ctx, dryrunContextKeyVal, dryrun)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
@ -25,76 +26,154 @@ func Warningf(format string, args ...interface{}) Warning {
|
|||
}
|
||||
|
||||
// Executor define contract for the steps of a workflow
|
||||
type Executor func() error
|
||||
type Executor func(ctx context.Context) error
|
||||
|
||||
// Conditional define contract for the conditional predicate
|
||||
type Conditional func() bool
|
||||
type Conditional func(ctx context.Context) bool
|
||||
|
||||
// NewInfoExecutor is an executor that logs messages
|
||||
func NewInfoExecutor(format string, args ...interface{}) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := Logger(ctx)
|
||||
logger.Infof(format, args...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewPipelineExecutor creates a new executor from a series of other executors
|
||||
func NewPipelineExecutor(executors ...Executor) Executor {
|
||||
return func() error {
|
||||
if executors == nil {
|
||||
return func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var rtn Executor
|
||||
for _, executor := range executors {
|
||||
if executor == nil {
|
||||
continue
|
||||
}
|
||||
err := executor()
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case Warning:
|
||||
log.Warning(err.Error())
|
||||
return nil
|
||||
default:
|
||||
log.Debugf("%+v", err)
|
||||
return err
|
||||
if rtn == nil {
|
||||
rtn = executor
|
||||
} else {
|
||||
rtn = rtn.Then(executor)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return rtn
|
||||
}
|
||||
|
||||
// NewConditionalExecutor creates a new executor based on conditions
|
||||
func NewConditionalExecutor(conditional Conditional, trueExecutor Executor, falseExecutor Executor) Executor {
|
||||
return func() error {
|
||||
if conditional() {
|
||||
return func(ctx context.Context) error {
|
||||
if conditional(ctx) {
|
||||
if trueExecutor != nil {
|
||||
return trueExecutor()
|
||||
return trueExecutor(ctx)
|
||||
}
|
||||
} else {
|
||||
if falseExecutor != nil {
|
||||
return falseExecutor()
|
||||
return falseExecutor(ctx)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func executeWithChan(executor Executor, errChan chan error) {
|
||||
errChan <- executor()
|
||||
}
|
||||
|
||||
// NewErrorExecutor creates a new executor that always errors out
|
||||
func NewErrorExecutor(err error) Executor {
|
||||
return func() error {
|
||||
return func(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// NewParallelExecutor creates a new executor from a parallel of other executors
|
||||
func NewParallelExecutor(executors ...Executor) Executor {
|
||||
return func() error {
|
||||
return func(ctx context.Context) error {
|
||||
errChan := make(chan error)
|
||||
|
||||
for _, executor := range executors {
|
||||
go executeWithChan(executor, errChan)
|
||||
go executor.ChannelError(errChan)(ctx)
|
||||
}
|
||||
|
||||
for i := 0; i < len(executors); i++ {
|
||||
err := <-errChan
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ChannelError sends error to errChan rather than returning error
|
||||
func (e Executor) ChannelError(errChan chan error) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
errChan <- e(ctx)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Then runs another executor if this executor succeeds
|
||||
func (e Executor) Then(then Executor) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
err := e(ctx)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case Warning:
|
||||
log.Warning(err.Error())
|
||||
default:
|
||||
log.Debugf("%+v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return then(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// If only runs this executor if conditional is true
|
||||
func (e Executor) If(conditional Conditional) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
if conditional(ctx) {
|
||||
return e(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// IfNot only runs this executor if conditional is true
|
||||
func (e Executor) IfNot(conditional Conditional) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
if !conditional(ctx) {
|
||||
return e(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// IfBool only runs this executor if conditional is true
|
||||
func (e Executor) IfBool(conditional bool) Executor {
|
||||
return e.If(func(ctx context.Context) bool {
|
||||
return conditional
|
||||
})
|
||||
}
|
||||
|
||||
// Finally adds an executor to run after other executor
|
||||
func (e Executor) Finally(finally Executor) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
err := e(ctx)
|
||||
err2 := finally(ctx)
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("Error occurred running finally: %v (original error: %v)", err2, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Not return an inverted conditional
|
||||
func (c Conditional) Not() Conditional {
|
||||
return func(ctx context.Context) bool {
|
||||
return !c(ctx)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
|
@ -10,58 +11,62 @@ import (
|
|||
func TestNewWorkflow(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// empty
|
||||
emptyWorkflow := NewPipelineExecutor()
|
||||
assert.Nil(emptyWorkflow())
|
||||
assert.Nil(emptyWorkflow(ctx))
|
||||
|
||||
// error case
|
||||
errorWorkflow := NewErrorExecutor(fmt.Errorf("test error"))
|
||||
assert.NotNil(errorWorkflow())
|
||||
assert.NotNil(errorWorkflow(ctx))
|
||||
|
||||
// multiple success case
|
||||
runcount := 0
|
||||
successWorkflow := NewPipelineExecutor(
|
||||
func() error {
|
||||
func(ctx context.Context) error {
|
||||
runcount++
|
||||
return nil
|
||||
},
|
||||
func() error {
|
||||
func(ctx context.Context) error {
|
||||
runcount++
|
||||
return nil
|
||||
})
|
||||
assert.Nil(successWorkflow())
|
||||
assert.Nil(successWorkflow(ctx))
|
||||
assert.Equal(2, runcount)
|
||||
}
|
||||
|
||||
func TestNewConditionalExecutor(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
trueCount := 0
|
||||
falseCount := 0
|
||||
|
||||
err := NewConditionalExecutor(func() bool {
|
||||
err := NewConditionalExecutor(func(ctx context.Context) bool {
|
||||
return false
|
||||
}, func() error {
|
||||
}, func(ctx context.Context) error {
|
||||
trueCount++
|
||||
return nil
|
||||
}, func() error {
|
||||
}, func(ctx context.Context) error {
|
||||
falseCount++
|
||||
return nil
|
||||
})()
|
||||
})(ctx)
|
||||
|
||||
assert.Nil(err)
|
||||
assert.Equal(0, trueCount)
|
||||
assert.Equal(1, falseCount)
|
||||
|
||||
err = NewConditionalExecutor(func() bool {
|
||||
err = NewConditionalExecutor(func(ctx context.Context) bool {
|
||||
return true
|
||||
}, func() error {
|
||||
}, func(ctx context.Context) error {
|
||||
trueCount++
|
||||
return nil
|
||||
}, func() error {
|
||||
}, func(ctx context.Context) error {
|
||||
falseCount++
|
||||
return nil
|
||||
})()
|
||||
})(ctx)
|
||||
|
||||
assert.Nil(err)
|
||||
assert.Equal(1, trueCount)
|
||||
|
@ -71,13 +76,15 @@ func TestNewConditionalExecutor(t *testing.T) {
|
|||
func TestNewParallelExecutor(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
count := 0
|
||||
emptyWorkflow := NewPipelineExecutor(func() error {
|
||||
emptyWorkflow := NewPipelineExecutor(func(ctx context.Context) error {
|
||||
count++
|
||||
return nil
|
||||
})
|
||||
|
||||
err := NewParallelExecutor(emptyWorkflow, emptyWorkflow)()
|
||||
err := NewParallelExecutor(emptyWorkflow, emptyWorkflow)(ctx)
|
||||
assert.Equal(2, count)
|
||||
|
||||
assert.Nil(err)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
@ -190,7 +191,7 @@ type NewGitCloneExecutorInput struct {
|
|||
|
||||
// NewGitCloneExecutor creates an executor to clone git repos
|
||||
func NewGitCloneExecutor(input NewGitCloneExecutorInput) Executor {
|
||||
return func() error {
|
||||
return func(ctx context.Context) error {
|
||||
input.Logger.Infof("git clone '%s' # ref=%s", input.URL, input.Ref)
|
||||
input.Logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
|
||||
|
||||
|
|
27
act/common/logger.go
Normal file
27
act/common/logger.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type loggerContextKey string
|
||||
|
||||
const loggerContextKeyVal = loggerContextKey("logrus.FieldLogger")
|
||||
|
||||
// Logger returns the appropriate logger for current context
|
||||
func Logger(ctx context.Context) logrus.FieldLogger {
|
||||
val := ctx.Value(loggerContextKeyVal)
|
||||
if val != nil {
|
||||
if logger, ok := val.(logrus.FieldLogger); ok {
|
||||
return logger
|
||||
}
|
||||
}
|
||||
return logrus.StandardLogger()
|
||||
}
|
||||
|
||||
// WithLogger adds a value to the context for the logger
|
||||
func WithLogger(ctx context.Context, logger logrus.FieldLogger) context.Context {
|
||||
return context.WithValue(ctx, loggerContextKeyVal, logger)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -16,16 +17,16 @@ import (
|
|||
|
||||
// NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function
|
||||
type NewDockerBuildExecutorInput struct {
|
||||
DockerExecutorInput
|
||||
ContextDir string
|
||||
ImageTag string
|
||||
}
|
||||
|
||||
// NewDockerBuildExecutor function to create a run executor for the container
|
||||
func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
|
||||
return func() error {
|
||||
input.Logger.Infof("docker build -t %s %s", input.ImageTag, input.ContextDir)
|
||||
if input.Dryrun {
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
logger.Infof("docker build -t %s %s", input.ImageTag, input.ContextDir)
|
||||
if common.Dryrun(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -33,9 +34,9 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.NegotiateAPIVersion(input.Ctx)
|
||||
cli.NegotiateAPIVersion(ctx)
|
||||
|
||||
input.Logger.Debugf("Building image from '%v'", input.ContextDir)
|
||||
logger.Debugf("Building image from '%v'", input.ContextDir)
|
||||
|
||||
tags := []string{input.ImageTag}
|
||||
options := types.ImageBuildOptions{
|
||||
|
@ -49,10 +50,10 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
|
|||
|
||||
defer buildContext.Close()
|
||||
|
||||
input.Logger.Debugf("Creating image from context dir '%s' with tag '%s'", input.ContextDir, input.ImageTag)
|
||||
resp, err := cli.ImageBuild(input.Ctx, buildContext, options)
|
||||
logger.Debugf("Creating image from context dir '%s' with tag '%s'", input.ContextDir, input.ImageTag)
|
||||
resp, err := cli.ImageBuild(ctx, buildContext, options)
|
||||
|
||||
err = input.logDockerResponse(resp.Body, err != nil)
|
||||
err = logDockerResponse(logger, resp.Body, err != nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,115 +0,0 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// DockerExecutorInput common input params
|
||||
type DockerExecutorInput struct {
|
||||
Ctx context.Context
|
||||
Logger *logrus.Entry
|
||||
Dryrun bool
|
||||
}
|
||||
|
||||
type dockerMessage struct {
|
||||
ID string `json:"id"`
|
||||
Stream string `json:"stream"`
|
||||
Error string `json:"error"`
|
||||
ErrorDetail struct {
|
||||
Message string
|
||||
}
|
||||
Status string `json:"status"`
|
||||
Progress string `json:"progress"`
|
||||
}
|
||||
|
||||
func (i *DockerExecutorInput) logDockerOutput(dockerResponse io.Reader) {
|
||||
w := i.Logger.Writer()
|
||||
_, err := stdcopy.StdCopy(w, w, dockerResponse)
|
||||
if err != nil {
|
||||
i.Logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *DockerExecutorInput) streamDockerOutput(dockerResponse io.Reader) {
|
||||
out := os.Stdout
|
||||
go func() {
|
||||
<-i.Ctx.Done()
|
||||
fmt.Println()
|
||||
}()
|
||||
|
||||
_, err := io.Copy(out, dockerResponse)
|
||||
if err != nil {
|
||||
i.Logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *DockerExecutorInput) writeLog(isError bool, format string, args ...interface{}) {
|
||||
if i.Logger == nil {
|
||||
return
|
||||
}
|
||||
if isError {
|
||||
i.Logger.Errorf(format, args...)
|
||||
} else {
|
||||
i.Logger.Debugf(format, args...)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (i *DockerExecutorInput) logDockerResponse(dockerResponse io.ReadCloser, isError bool) error {
|
||||
if dockerResponse == nil {
|
||||
return nil
|
||||
}
|
||||
defer dockerResponse.Close()
|
||||
|
||||
scanner := bufio.NewScanner(dockerResponse)
|
||||
msg := dockerMessage{}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
|
||||
msg.ID = ""
|
||||
msg.Stream = ""
|
||||
msg.Error = ""
|
||||
msg.ErrorDetail.Message = ""
|
||||
msg.Status = ""
|
||||
msg.Progress = ""
|
||||
|
||||
if err := json.Unmarshal(line, &msg); err != nil {
|
||||
i.writeLog(false, "Unable to unmarshal line [%s] ==> %v", string(line), err)
|
||||
continue
|
||||
}
|
||||
|
||||
if msg.Error != "" {
|
||||
i.writeLog(isError, "%s", msg.Error)
|
||||
return errors.New(msg.Error)
|
||||
}
|
||||
|
||||
if msg.ErrorDetail.Message != "" {
|
||||
i.writeLog(isError, "%s", msg.ErrorDetail.Message)
|
||||
return errors.New(msg.Error)
|
||||
}
|
||||
|
||||
if msg.Status != "" {
|
||||
if msg.Progress != "" {
|
||||
i.writeLog(isError, "%s :: %s :: %s\n", msg.Status, msg.ID, msg.Progress)
|
||||
} else {
|
||||
i.writeLog(isError, "%s :: %s\n", msg.Status, msg.ID)
|
||||
}
|
||||
} else if msg.Stream != "" {
|
||||
i.writeLog(isError, msg.Stream)
|
||||
} else {
|
||||
i.writeLog(false, "Unable to handle line: %s", string(line))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
117
act/container/docker_logger.go
Normal file
117
act/container/docker_logger.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
)
|
||||
|
||||
type dockerMessage struct {
|
||||
ID string `json:"id"`
|
||||
Stream string `json:"stream"`
|
||||
Error string `json:"error"`
|
||||
ErrorDetail struct {
|
||||
Message string
|
||||
}
|
||||
Status string `json:"status"`
|
||||
Progress string `json:"progress"`
|
||||
}
|
||||
|
||||
func logDockerOutput(ctx context.Context, dockerResponse io.Reader) {
|
||||
logger := common.Logger(ctx)
|
||||
if entry, ok := logger.(*logrus.Entry); ok {
|
||||
w := entry.Writer()
|
||||
_, err := stdcopy.StdCopy(w, w, dockerResponse)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
} else if lgr, ok := logger.(*logrus.Logger); ok {
|
||||
w := lgr.Writer()
|
||||
_, err := stdcopy.StdCopy(w, w, dockerResponse)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
} else {
|
||||
logrus.Errorf("Unable to get writer from logger (type=%T)", logger)
|
||||
}
|
||||
}
|
||||
|
||||
func streamDockerOutput(ctx context.Context, dockerResponse io.Reader) {
|
||||
out := os.Stdout
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
fmt.Println()
|
||||
}()
|
||||
|
||||
_, err := io.Copy(out, dockerResponse)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error {
|
||||
if dockerResponse == nil {
|
||||
return nil
|
||||
}
|
||||
defer dockerResponse.Close()
|
||||
|
||||
scanner := bufio.NewScanner(dockerResponse)
|
||||
msg := dockerMessage{}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
|
||||
msg.ID = ""
|
||||
msg.Stream = ""
|
||||
msg.Error = ""
|
||||
msg.ErrorDetail.Message = ""
|
||||
msg.Status = ""
|
||||
msg.Progress = ""
|
||||
|
||||
if err := json.Unmarshal(line, &msg); err != nil {
|
||||
writeLog(logger, false, "Unable to unmarshal line [%s] ==> %v", string(line), err)
|
||||
continue
|
||||
}
|
||||
|
||||
if msg.Error != "" {
|
||||
writeLog(logger, isError, "%s", msg.Error)
|
||||
return errors.New(msg.Error)
|
||||
}
|
||||
|
||||
if msg.ErrorDetail.Message != "" {
|
||||
writeLog(logger, isError, "%s", msg.ErrorDetail.Message)
|
||||
return errors.New(msg.Error)
|
||||
}
|
||||
|
||||
if msg.Status != "" {
|
||||
if msg.Progress != "" {
|
||||
writeLog(logger, isError, "%s :: %s :: %s\n", msg.Status, msg.ID, msg.Progress)
|
||||
} else {
|
||||
writeLog(logger, isError, "%s :: %s\n", msg.Status, msg.ID)
|
||||
}
|
||||
} else if msg.Stream != "" {
|
||||
writeLog(logger, isError, msg.Stream)
|
||||
} else {
|
||||
writeLog(logger, false, "Unable to handle line: %s", string(line))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeLog(logger logrus.FieldLogger, isError bool, format string, args ...interface{}) {
|
||||
if isError {
|
||||
logger.Errorf(format, args...)
|
||||
} else {
|
||||
logger.Debugf(format, args...)
|
||||
}
|
||||
}
|
|
@ -1,40 +1,60 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// NewDockerPullExecutorInput the input for the NewDockerPullExecutor function
|
||||
type NewDockerPullExecutorInput struct {
|
||||
DockerExecutorInput
|
||||
Image string
|
||||
ForcePull bool
|
||||
}
|
||||
|
||||
// NewDockerPullExecutor function to create a run executor for the container
|
||||
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
||||
return func() error {
|
||||
input.Logger.Infof("docker pull %v", input.Image)
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
logger.Infof("docker pull %v", input.Image)
|
||||
|
||||
if input.Dryrun {
|
||||
if common.Dryrun(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
pull := input.ForcePull
|
||||
if !pull {
|
||||
imageExists, err := ImageExistsLocally(ctx, input.Image)
|
||||
log.Debugf("Image exists? %v", imageExists)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to determine if image already exists for image %q", input.Image)
|
||||
}
|
||||
|
||||
if !imageExists {
|
||||
pull = true
|
||||
}
|
||||
}
|
||||
|
||||
if !pull {
|
||||
return nil
|
||||
}
|
||||
|
||||
imageRef := cleanImage(input.Image)
|
||||
input.Logger.Debugf("pulling image '%v'", imageRef)
|
||||
logger.Debugf("pulling image '%v'", imageRef)
|
||||
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.NegotiateAPIVersion(input.Ctx)
|
||||
cli.NegotiateAPIVersion(ctx)
|
||||
|
||||
reader, err := cli.ImagePull(input.Ctx, imageRef, types.ImagePullOptions{})
|
||||
_ = input.logDockerResponse(reader, err != nil)
|
||||
reader, err := cli.ImagePull(ctx, imageRef, types.ImagePullOptions{})
|
||||
_ = logDockerResponse(logger, reader, err != nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -10,12 +10,12 @@ import (
|
|||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// NewDockerRunExecutorInput the input for the NewDockerRunExecutor function
|
||||
type NewDockerRunExecutorInput struct {
|
||||
DockerExecutorInput
|
||||
Image string
|
||||
Entrypoint []string
|
||||
Cmd []string
|
||||
|
@ -30,64 +30,98 @@ type NewDockerRunExecutorInput struct {
|
|||
|
||||
// NewDockerRunExecutor function to create a run executor for the container
|
||||
func NewDockerRunExecutor(input NewDockerRunExecutorInput) common.Executor {
|
||||
return func() error {
|
||||
cr := new(containerReference)
|
||||
cr.input = input
|
||||
|
||||
input.Logger.Infof("docker run image=%s entrypoint=%+q cmd=%+q", input.Image, input.Entrypoint, input.Cmd)
|
||||
if input.Dryrun {
|
||||
return common.
|
||||
NewInfoExecutor("docker run image=%s entrypoint=%+q cmd=%+q", input.Image, input.Entrypoint, input.Cmd).
|
||||
Then(
|
||||
common.NewPipelineExecutor(
|
||||
cr.connect(),
|
||||
cr.find(),
|
||||
cr.remove().IfBool(!input.ReuseContainers),
|
||||
cr.create(),
|
||||
cr.copyContent(),
|
||||
cr.attach(),
|
||||
cr.start(),
|
||||
cr.wait(),
|
||||
).Finally(
|
||||
cr.remove().IfBool(!input.ReuseContainers),
|
||||
).IfNot(common.Dryrun),
|
||||
)
|
||||
}
|
||||
|
||||
type containerReference struct {
|
||||
input NewDockerRunExecutorInput
|
||||
cli *client.Client
|
||||
id string
|
||||
}
|
||||
|
||||
func (cr *containerReference) connect() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
cli.NegotiateAPIVersion(ctx)
|
||||
cr.cli = cli
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (cr *containerReference) find() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
containers, err := cr.cli.ContainerList(ctx, types.ContainerListOptions{
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, container := range containers {
|
||||
for _, name := range container.Names {
|
||||
if name[1:] == cr.input.Name {
|
||||
cr.id = container.ID
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cr.id = ""
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (cr *containerReference) remove() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
if cr.id == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
logger := common.Logger(ctx)
|
||||
err := cr.cli.ContainerRemove(context.Background(), cr.id, types.ContainerRemoveOptions{
|
||||
RemoveVolumes: true,
|
||||
Force: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
cli.NegotiateAPIVersion(input.Ctx)
|
||||
cr.id = ""
|
||||
|
||||
// check if container exists
|
||||
containerID, err := findContainer(input, cli, input.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if we have an old container and we aren't reusing, remove it!
|
||||
if !input.ReuseContainers && containerID != "" {
|
||||
input.Logger.Debugf("Found existing container for %s...removing", input.Name)
|
||||
removeContainer(input, cli, containerID)
|
||||
containerID = ""
|
||||
}
|
||||
|
||||
// create a new container if we don't have one to reuse
|
||||
if containerID == "" {
|
||||
containerID, err = createContainer(input, cli)
|
||||
if err != nil {
|
||||
return err
|
||||
logger.Debugf("Removed container: %v", cr.id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// be sure to cleanup container if we aren't reusing
|
||||
if !input.ReuseContainers {
|
||||
defer removeContainer(input, cli, containerID)
|
||||
func (cr *containerReference) create() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
if cr.id != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
executor := common.NewPipelineExecutor(
|
||||
func() error {
|
||||
return copyContentToContainer(input, cli, containerID)
|
||||
}, func() error {
|
||||
return attachContainer(input, cli, containerID)
|
||||
}, func() error {
|
||||
return startContainer(input, cli, containerID)
|
||||
}, func() error {
|
||||
return waitContainer(input, cli, containerID)
|
||||
},
|
||||
)
|
||||
return executor()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func createContainer(input NewDockerRunExecutorInput, cli *client.Client) (string, error) {
|
||||
logger := common.Logger(ctx)
|
||||
isTerminal := terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
|
||||
input := cr.input
|
||||
config := &container.Config{
|
||||
Image: input.Image,
|
||||
Cmd: input.Cmd,
|
||||
|
@ -104,102 +138,83 @@ func createContainer(input NewDockerRunExecutorInput, cli *client.Client) (strin
|
|||
}
|
||||
}
|
||||
|
||||
resp, err := cli.ContainerCreate(input.Ctx, config, &container.HostConfig{
|
||||
resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{
|
||||
Binds: input.Binds,
|
||||
}, nil, input.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
input.Logger.Debugf("Created container name=%s id=%v from image %v", input.Name, resp.ID, input.Image)
|
||||
input.Logger.Debugf("ENV ==> %v", input.Env)
|
||||
logger.Debugf("Created container name=%s id=%v from image %v", input.Name, resp.ID, input.Image)
|
||||
logger.Debugf("ENV ==> %v", input.Env)
|
||||
|
||||
return resp.ID, nil
|
||||
cr.id = resp.ID
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func findContainer(input NewDockerRunExecutorInput, cli *client.Client, containerName string) (string, error) {
|
||||
containers, err := cli.ContainerList(input.Ctx, types.ContainerListOptions{
|
||||
All: true,
|
||||
})
|
||||
func (cr *containerReference) copyContent() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
for dstPath, srcReader := range cr.input.Content {
|
||||
logger.Debugf("Extracting content to '%s'", dstPath)
|
||||
err := cr.cli.CopyToContainer(ctx, cr.id, dstPath, srcReader, types.CopyToContainerOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, container := range containers {
|
||||
for _, name := range container.Names {
|
||||
if name[1:] == containerName {
|
||||
return container.ID, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func removeContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) {
|
||||
err := cli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{
|
||||
RemoveVolumes: true,
|
||||
Force: true,
|
||||
})
|
||||
if err != nil {
|
||||
input.Logger.Errorf("%v", err)
|
||||
}
|
||||
|
||||
input.Logger.Debugf("Removed container: %v", containerID)
|
||||
}
|
||||
|
||||
func copyContentToContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error {
|
||||
for dstPath, srcReader := range input.Content {
|
||||
input.Logger.Debugf("Extracting content to '%s'", dstPath)
|
||||
err := cli.CopyToContainer(input.Ctx, containerID, dstPath, srcReader, types.CopyToContainerOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func attachContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error {
|
||||
out, err := cli.ContainerAttach(input.Ctx, containerID, types.ContainerAttachOptions{
|
||||
func (cr *containerReference) attach() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
out, err := cr.cli.ContainerAttach(ctx, cr.id, types.ContainerAttachOptions{
|
||||
Stream: true,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
isTerminal := terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
if !isTerminal || os.Getenv("NORAW") != "" {
|
||||
go input.logDockerOutput(out.Reader)
|
||||
go logDockerOutput(ctx, out.Reader)
|
||||
} else {
|
||||
go input.streamDockerOutput(out.Reader)
|
||||
go streamDockerOutput(ctx, out.Reader)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func startContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error {
|
||||
input.Logger.Debugf("STARTING image=%s entrypoint=%s cmd=%v", input.Image, input.Entrypoint, input.Cmd)
|
||||
|
||||
if err := cli.ContainerStart(input.Ctx, containerID, types.ContainerStartOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
input.Logger.Debugf("Started container: %v", containerID)
|
||||
func (cr *containerReference) start() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
logger.Debugf("STARTING image=%s entrypoint=%s cmd=%v", cr.input.Image, cr.input.Entrypoint, cr.input.Cmd)
|
||||
|
||||
if err := cr.cli.ContainerStart(ctx, cr.id, types.ContainerStartOptions{}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.Debugf("Started container: %v", cr.id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func waitContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error {
|
||||
statusCh, errCh := cli.ContainerWait(input.Ctx, containerID, container.WaitConditionNotRunning)
|
||||
func (cr *containerReference) wait() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
statusCh, errCh := cr.cli.ContainerWait(ctx, cr.id, container.WaitConditionNotRunning)
|
||||
var statusCode int64
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case status := <-statusCh:
|
||||
statusCode = status.StatusCode
|
||||
}
|
||||
|
||||
input.Logger.Debugf("Return status: %v", statusCode)
|
||||
logger.Debugf("Return status: %v", statusCode)
|
||||
|
||||
if statusCode == 0 {
|
||||
return nil
|
||||
|
@ -209,3 +224,4 @@ func waitContainer(input NewDockerRunExecutorInput, cli *client.Client, containe
|
|||
|
||||
return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,27 +30,21 @@ func TestNewDockerRunExecutor(t *testing.T) {
|
|||
logger.SetOutput(buf)
|
||||
logger.SetFormatter(&rawFormatter{})
|
||||
|
||||
ctx := common.WithLogger(context.Background(), logger)
|
||||
|
||||
runner := NewDockerRunExecutor(NewDockerRunExecutorInput{
|
||||
DockerExecutorInput: DockerExecutorInput{
|
||||
Ctx: context.TODO(),
|
||||
Logger: logrus.NewEntry(logger),
|
||||
},
|
||||
Image: "hello-world",
|
||||
})
|
||||
|
||||
puller := NewDockerPullExecutor(NewDockerPullExecutorInput{
|
||||
DockerExecutorInput: DockerExecutorInput{
|
||||
Ctx: context.TODO(),
|
||||
Logger: logrus.NewEntry(noopLogger),
|
||||
},
|
||||
Image: "hello-world",
|
||||
})
|
||||
|
||||
pipeline := common.NewPipelineExecutor(puller, runner)
|
||||
err := pipeline()
|
||||
err := pipeline(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := `docker run image=hello-world entrypoint=[] cmd=[]Hello from Docker!`
|
||||
expected := `docker pull hello-worlddocker run image=hello-world entrypoint=[] cmd=[]Hello from Docker!`
|
||||
actual := buf.String()
|
||||
assert.Equal(t, expected, actual[:len(expected)])
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
|
@ -33,6 +34,19 @@ type Run struct {
|
|||
JobID string
|
||||
}
|
||||
|
||||
func (r *Run) String() string {
|
||||
jobName := r.Job().Name
|
||||
if jobName == "" {
|
||||
jobName = r.JobID
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", r.Workflow.Name, jobName)
|
||||
}
|
||||
|
||||
// Job returns the job for this Run
|
||||
func (r *Run) Job() *Job {
|
||||
return r.Workflow.GetJob(r.JobID)
|
||||
}
|
||||
|
||||
// NewWorkflowPlanner will load all workflows from a directory
|
||||
func NewWorkflowPlanner(dirname string) (WorkflowPlanner, error) {
|
||||
log.Debugf("Loading workflows from '%s'", dirname)
|
||||
|
@ -55,6 +69,9 @@ func NewWorkflowPlanner(dirname string) (WorkflowPlanner, error) {
|
|||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
if workflow.Name == "" {
|
||||
workflow.Name = file.Name()
|
||||
}
|
||||
wp.workflows = append(wp.workflows, workflow)
|
||||
f.Close()
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
@ -23,6 +25,19 @@ type Job struct {
|
|||
If string `yaml:"if"`
|
||||
Steps []*Step `yaml:"steps"`
|
||||
TimeoutMinutes int64 `yaml:"timeout-minutes"`
|
||||
Container *ContainerSpec `yaml:"container"`
|
||||
Services map[string]*ContainerSpec `yaml:"services"`
|
||||
}
|
||||
|
||||
// ContainerSpec is the specification of the container to use for the job
|
||||
type ContainerSpec struct {
|
||||
Image string `yaml:"image"`
|
||||
Env map[string]string `yaml:"env"`
|
||||
Ports []int `yaml:"ports"`
|
||||
Volumes []string `yaml:"volumes"`
|
||||
Options string `yaml:"options"`
|
||||
Entrypoint string
|
||||
Args string
|
||||
}
|
||||
|
||||
// Step is the structure of one step in a job
|
||||
|
@ -40,6 +55,19 @@ type Step struct {
|
|||
TimeoutMinutes int64 `yaml:"timeout-minutes"`
|
||||
}
|
||||
|
||||
// GetEnv gets the env for a step
|
||||
func (s *Step) GetEnv() map[string]string {
|
||||
rtnEnv := make(map[string]string)
|
||||
for k, v := range s.Env {
|
||||
rtnEnv[k] = v
|
||||
}
|
||||
for k, v := range s.With {
|
||||
envKey := fmt.Sprintf("INPUT_%s", strings.ToUpper(k))
|
||||
rtnEnv[envKey] = v
|
||||
}
|
||||
return rtnEnv
|
||||
}
|
||||
|
||||
// ReadWorkflow returns a list of jobs for a given workflow file reader
|
||||
func ReadWorkflow(in io.Reader) (*Workflow, error) {
|
||||
w := new(Workflow)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package actions
|
||||
package runner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -11,15 +11,6 @@ import (
|
|||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
type actionLogFormatter struct {
|
||||
}
|
||||
|
||||
var formatter *actionLogFormatter
|
||||
|
||||
func init() {
|
||||
formatter = new(actionLogFormatter)
|
||||
}
|
||||
|
||||
const (
|
||||
//nocolor = 0
|
||||
red = 31
|
||||
|
@ -29,16 +20,20 @@ const (
|
|||
gray = 37
|
||||
)
|
||||
|
||||
func newActionLogger(actionName string, dryrun bool) *logrus.Entry {
|
||||
// NewJobLogger gets the logger for the Job
|
||||
func NewJobLogger(jobName string, dryrun bool) logrus.FieldLogger {
|
||||
logger := logrus.New()
|
||||
logger.SetFormatter(formatter)
|
||||
logger.SetFormatter(new(jobLogFormatter))
|
||||
logger.SetOutput(os.Stdout)
|
||||
logger.SetLevel(logrus.GetLevel())
|
||||
rtn := logger.WithFields(logrus.Fields{"action_name": actionName, "dryrun": dryrun})
|
||||
rtn := logger.WithFields(logrus.Fields{"job_name": jobName, "dryrun": dryrun})
|
||||
return rtn
|
||||
}
|
||||
|
||||
func (f *actionLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
type jobLogFormatter struct {
|
||||
}
|
||||
|
||||
func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
b := &bytes.Buffer{}
|
||||
|
||||
if f.isColored(entry) {
|
||||
|
@ -51,7 +46,7 @@ func (f *actionLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
|||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func (f *actionLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
|
||||
func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
|
||||
var levelColor int
|
||||
switch entry.Level {
|
||||
case logrus.DebugLevel, logrus.TraceLevel:
|
||||
|
@ -65,27 +60,27 @@ func (f *actionLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry)
|
|||
}
|
||||
|
||||
entry.Message = strings.TrimSuffix(entry.Message, "\n")
|
||||
actionName := entry.Data["action_name"]
|
||||
jobName := entry.Data["job_name"]
|
||||
|
||||
if entry.Data["dryrun"] == true {
|
||||
fmt.Fprintf(b, "\x1b[%dm*DRYRUN* \x1b[%dm[%s] \x1b[0m%s", green, levelColor, actionName, entry.Message)
|
||||
fmt.Fprintf(b, "\x1b[%dm*DRYRUN* \x1b[%dm[%s] \x1b[0m%s", green, levelColor, jobName, entry.Message)
|
||||
} else {
|
||||
fmt.Fprintf(b, "\x1b[%dm[%s] \x1b[0m%s", levelColor, actionName, entry.Message)
|
||||
fmt.Fprintf(b, "\x1b[%dm[%s] \x1b[0m%s", levelColor, jobName, entry.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *actionLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
|
||||
func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
|
||||
entry.Message = strings.TrimSuffix(entry.Message, "\n")
|
||||
actionName := entry.Data["action_name"]
|
||||
jobName := entry.Data["job_name"]
|
||||
|
||||
if entry.Data["dryrun"] == true {
|
||||
fmt.Fprintf(b, "*DRYRUN* [%s] %s", actionName, entry.Message)
|
||||
fmt.Fprintf(b, "*DRYRUN* [%s] %s", jobName, entry.Message)
|
||||
} else {
|
||||
fmt.Fprintf(b, "[%s] %s", actionName, entry.Message)
|
||||
fmt.Fprintf(b, "[%s] %s", jobName, entry.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *actionLogFormatter) isColored(entry *logrus.Entry) bool {
|
||||
func (f *jobLogFormatter) isColored(entry *logrus.Entry) bool {
|
||||
|
||||
isColored := checkIfTerminal(entry.Logger.Out)
|
||||
|
269
act/runner/run_context.go
Normal file
269
act/runner/run_context.go
Normal file
|
@ -0,0 +1,269 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/nektos/act/pkg/container"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// RunContext contains info about current job
|
||||
type RunContext struct {
|
||||
Config *Config
|
||||
Run *model.Run
|
||||
EventJSON string
|
||||
Env map[string]string
|
||||
Outputs map[string]string
|
||||
Tempdir string
|
||||
}
|
||||
|
||||
// GetEnv returns the env for the context
|
||||
func (rc *RunContext) GetEnv() map[string]string {
|
||||
if rc.Env == nil {
|
||||
rc.Env = mergeMaps(rc.Run.Workflow.Env, rc.Run.Job().Env)
|
||||
}
|
||||
return rc.Env
|
||||
}
|
||||
|
||||
// StepEnv returns the env for a step
|
||||
func (rc *RunContext) StepEnv(step *model.Step) map[string]string {
|
||||
env := make(map[string]string)
|
||||
env["HOME"] = "/github/home"
|
||||
env["GITHUB_WORKFLOW"] = rc.Run.Workflow.Name
|
||||
env["GITHUB_RUN_ID"] = "1"
|
||||
env["GITHUB_RUN_NUMBER"] = "1"
|
||||
env["GITHUB_ACTION"] = step.ID
|
||||
env["GITHUB_ACTOR"] = "nektos/act"
|
||||
|
||||
repoPath := rc.Config.Workdir
|
||||
repo, err := common.FindGithubRepo(repoPath)
|
||||
if err != nil {
|
||||
log.Warningf("unable to get git repo: %v", err)
|
||||
} else {
|
||||
env["GITHUB_REPOSITORY"] = repo
|
||||
}
|
||||
env["GITHUB_EVENT_NAME"] = rc.Config.EventName
|
||||
env["GITHUB_EVENT_PATH"] = "/github/workflow/event.json"
|
||||
env["GITHUB_WORKSPACE"] = "/github/workspace"
|
||||
|
||||
_, rev, err := common.FindGitRevision(repoPath)
|
||||
if err != nil {
|
||||
log.Warningf("unable to get git revision: %v", err)
|
||||
} else {
|
||||
env["GITHUB_SHA"] = rev
|
||||
}
|
||||
|
||||
ref, err := common.FindGitRef(repoPath)
|
||||
if err != nil {
|
||||
log.Warningf("unable to get git ref: %v", err)
|
||||
} else {
|
||||
log.Infof("using github ref: %s", ref)
|
||||
env["GITHUB_REF"] = ref
|
||||
}
|
||||
job := rc.Run.Job()
|
||||
if job.Container != nil {
|
||||
return mergeMaps(rc.GetEnv(), job.Container.Env, step.GetEnv(), env)
|
||||
}
|
||||
return mergeMaps(rc.GetEnv(), step.GetEnv(), env)
|
||||
}
|
||||
|
||||
// Close cleans up temp dir
|
||||
func (rc *RunContext) Close(ctx context.Context) error {
|
||||
return os.RemoveAll(rc.Tempdir)
|
||||
}
|
||||
|
||||
// Executor returns a pipeline executor for all the steps in the job
|
||||
func (rc *RunContext) Executor() common.Executor {
|
||||
steps := make([]common.Executor, 0)
|
||||
steps = append(steps, rc.setupTempDir())
|
||||
|
||||
for _, step := range rc.Run.Job().Steps {
|
||||
containerSpec := new(model.ContainerSpec)
|
||||
|
||||
var stepExecutor common.Executor
|
||||
if step.Run != "" {
|
||||
stepExecutor = common.NewPipelineExecutor(
|
||||
rc.setupContainerSpec(step, containerSpec),
|
||||
rc.pullImage(containerSpec),
|
||||
rc.runContainer(containerSpec),
|
||||
)
|
||||
} else if step.Uses != "" {
|
||||
stepExecutor = common.NewErrorExecutor(fmt.Errorf("Not yet implemented - job:%s step:%+v", rc.Run, step))
|
||||
// clone action repo
|
||||
// read action.yaml
|
||||
// if runs.using == node12, start node12 container and run `main`
|
||||
// if runs.using == docker, pull `image` and run
|
||||
// set inputs as env
|
||||
// caputre output
|
||||
} else {
|
||||
stepExecutor = common.NewErrorExecutor(fmt.Errorf("Unable to determine how to run job:%s step:%+v", rc.Run, step))
|
||||
}
|
||||
steps = append(steps, stepExecutor)
|
||||
}
|
||||
return common.NewPipelineExecutor(steps...).Finally(rc.Close)
|
||||
}
|
||||
|
||||
func (rc *RunContext) setupContainerSpec(step *model.Step, containerSpec *model.ContainerSpec) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
job := rc.Run.Job()
|
||||
|
||||
containerSpec.Env = rc.StepEnv(step)
|
||||
|
||||
if step.Uses != "" {
|
||||
containerSpec.Image = step.Uses
|
||||
} else if job.Container != nil {
|
||||
containerSpec.Image = job.Container.Image
|
||||
containerSpec.Args = step.Run
|
||||
containerSpec.Ports = job.Container.Ports
|
||||
containerSpec.Volumes = job.Container.Volumes
|
||||
containerSpec.Options = job.Container.Options
|
||||
} else if step.Run != "" {
|
||||
containerSpec.Image = platformImage(job.RunsOn)
|
||||
containerSpec.Args = step.Run
|
||||
} else {
|
||||
return fmt.Errorf("Unable to setup container for %s", step)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func platformImage(platform string) string {
|
||||
switch platform {
|
||||
case "ubuntu-latest", "ubuntu-18.04":
|
||||
return "ubuntu:18.04"
|
||||
case "ubuntu-16.04":
|
||||
return "ubuntu:16.04"
|
||||
case "windows-latest", "windows-2019", "macos-latest", "macos-10.15":
|
||||
return ""
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func mergeMaps(maps ...map[string]string) map[string]string {
|
||||
rtnMap := make(map[string]string)
|
||||
for _, m := range maps {
|
||||
for k, v := range m {
|
||||
rtnMap[k] = v
|
||||
}
|
||||
}
|
||||
return rtnMap
|
||||
}
|
||||
|
||||
func (rc *RunContext) setupTempDir() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
var err error
|
||||
rc.Tempdir, err = ioutil.TempDir("", "act-")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) pullImage(containerSpec *model.ContainerSpec) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
return container.NewDockerPullExecutor(container.NewDockerPullExecutorInput{
|
||||
Image: containerSpec.Image,
|
||||
ForcePull: rc.Config.ForcePull,
|
||||
})(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) runContainer(containerSpec *model.ContainerSpec) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
ghReader, err := rc.createGithubTarball()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
envList := make([]string, 0)
|
||||
for k, v := range containerSpec.Env {
|
||||
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
var cmd, entrypoint []string
|
||||
if containerSpec.Args != "" {
|
||||
cmd = []string{
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
containerSpec.Args,
|
||||
}
|
||||
}
|
||||
if containerSpec.Entrypoint != "" {
|
||||
entrypoint = strings.Fields(containerSpec.Entrypoint)
|
||||
}
|
||||
|
||||
return container.NewDockerRunExecutor(container.NewDockerRunExecutorInput{
|
||||
Cmd: cmd,
|
||||
Entrypoint: entrypoint,
|
||||
Image: containerSpec.Image,
|
||||
WorkingDir: "/github/workspace",
|
||||
Env: envList,
|
||||
Name: rc.createContainerName(),
|
||||
Binds: []string{
|
||||
fmt.Sprintf("%s:%s", rc.Config.Workdir, "/github/workspace"),
|
||||
fmt.Sprintf("%s:%s", rc.Tempdir, "/github/home"),
|
||||
fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"),
|
||||
},
|
||||
Content: map[string]io.Reader{"/github": ghReader},
|
||||
ReuseContainers: rc.Config.ReuseContainers,
|
||||
})(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) createGithubTarball() (io.Reader, error) {
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
var files = []struct {
|
||||
Name string
|
||||
Mode int64
|
||||
Body string
|
||||
}{
|
||||
{"workflow/event.json", 0644, rc.EventJSON},
|
||||
}
|
||||
for _, file := range files {
|
||||
log.Debugf("Writing entry to tarball %s len:%d", file.Name, len(rc.EventJSON))
|
||||
hdr := &tar.Header{
|
||||
Name: file.Name,
|
||||
Mode: file.Mode,
|
||||
Size: int64(len(rc.EventJSON)),
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := tw.Write([]byte(rc.EventJSON)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := tw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &buf, nil
|
||||
|
||||
}
|
||||
|
||||
func (rc *RunContext) createContainerName() string {
|
||||
containerName := regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(rc.Run.String(), "-")
|
||||
|
||||
prefix := fmt.Sprintf("%s-", trimToLen(filepath.Base(rc.Config.Workdir), 10))
|
||||
suffix := ""
|
||||
containerName = trimToLen(containerName, 30-(len(prefix)+len(suffix)))
|
||||
return fmt.Sprintf("%s%s%s", prefix, containerName, suffix)
|
||||
}
|
||||
|
||||
func trimToLen(s string, l int) string {
|
||||
if len(s) > l {
|
||||
return s[:l]
|
||||
}
|
||||
return s
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
|
@ -12,18 +10,13 @@ import (
|
|||
|
||||
// Runner provides capabilities to run GitHub actions
|
||||
type Runner interface {
|
||||
PlanRunner
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// PlanRunner to run a specific actions
|
||||
type PlanRunner interface {
|
||||
RunPlan(plan *model.Plan) error
|
||||
NewPlanExecutor(plan *model.Plan) common.Executor
|
||||
NewRunExecutor(run *model.Run) common.Executor
|
||||
}
|
||||
|
||||
// Config contains the config for a new runner
|
||||
type Config struct {
|
||||
Dryrun bool // don't start any of the containers
|
||||
Workdir string // path to working directory
|
||||
EventName string // name of event to run
|
||||
EventPath string // path to JSON file to use for event.json in containers
|
||||
ReuseContainers bool // reuse containers to maintain state
|
||||
|
@ -32,57 +25,44 @@ type Config struct {
|
|||
|
||||
type runnerImpl struct {
|
||||
config *Config
|
||||
tempDir string
|
||||
eventJSON string
|
||||
}
|
||||
|
||||
// NewRunner Creates a new Runner
|
||||
func NewRunner(runnerConfig *Config) (Runner, error) {
|
||||
// New Creates a new Runner
|
||||
func New(runnerConfig *Config) (Runner, error) {
|
||||
runner := &runnerImpl{
|
||||
config: runnerConfig,
|
||||
}
|
||||
|
||||
init := common.NewPipelineExecutor(
|
||||
runner.setupTempDir,
|
||||
runner.setupEvent,
|
||||
)
|
||||
|
||||
return runner, init()
|
||||
}
|
||||
|
||||
func (runner *runnerImpl) setupTempDir() error {
|
||||
var err error
|
||||
runner.tempDir, err = ioutil.TempDir("", "act-")
|
||||
return err
|
||||
}
|
||||
|
||||
func (runner *runnerImpl) setupEvent() error {
|
||||
runner.eventJSON = "{}"
|
||||
if runner.config.EventPath != "" {
|
||||
if runnerConfig.EventPath != "" {
|
||||
log.Debugf("Reading event.json from %s", runner.config.EventPath)
|
||||
eventJSONBytes, err := ioutil.ReadFile(runner.config.EventPath)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
runner.eventJSON = string(eventJSONBytes)
|
||||
}
|
||||
return nil
|
||||
return runner, nil
|
||||
}
|
||||
|
||||
func (runner *runnerImpl) RunPlan(plan *model.Plan) error {
|
||||
func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||
pipeline := make([]common.Executor, 0)
|
||||
for _, stage := range plan.Stages {
|
||||
stageExecutor := make([]common.Executor, 0)
|
||||
for _, run := range stage.Runs {
|
||||
stageExecutor = append(stageExecutor, runner.newRunExecutor(run))
|
||||
stageExecutor = append(stageExecutor, runner.NewRunExecutor(run))
|
||||
}
|
||||
pipeline = append(pipeline, common.NewParallelExecutor(stageExecutor...))
|
||||
}
|
||||
|
||||
executor := common.NewPipelineExecutor(pipeline...)
|
||||
return executor()
|
||||
return common.NewPipelineExecutor(pipeline...)
|
||||
}
|
||||
|
||||
func (runner *runnerImpl) Close() error {
|
||||
return os.RemoveAll(runner.tempDir)
|
||||
func (runner *runnerImpl) NewRunExecutor(run *model.Run) common.Executor {
|
||||
rc := new(RunContext)
|
||||
rc.Config = runner.config
|
||||
rc.Run = run
|
||||
rc.EventJSON = runner.eventJSON
|
||||
return rc.Executor()
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
"github.com/nektos/act/pkg/container"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -34,7 +33,6 @@ func (runner *runnerImpl) newRunExecutor(run *model.Run) common.Executor {
|
|||
return common.NewPipelineExecutor(executors...)
|
||||
}
|
||||
|
||||
/*
|
||||
func (runner *runnerImpl) addImageExecutor(action *Action, executors *[]common.Executor) (string, error) {
|
||||
var image string
|
||||
logger := newActionLogger(action.Identifier, runner.config.Dryrun)
|
||||
|
@ -111,7 +109,6 @@ func (runner *runnerImpl) addImageExecutor(action *Action, executors *[]common.E
|
|||
|
||||
return image, nil
|
||||
}
|
||||
*/
|
||||
|
||||
func (runner *runnerImpl) addRunExecutor(action *Action, image string, executors *[]common.Executor) error {
|
||||
logger := newActionLogger(action.Identifier, runner.config.Dryrun)
|
||||
|
@ -141,7 +138,11 @@ func (runner *runnerImpl) addRunExecutor(action *Action, image string, executors
|
|||
|
||||
var cmd, entrypoint []string
|
||||
if action.Args != nil {
|
||||
cmd = action.Args.Split()
|
||||
cmd = []string{
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
action.Args,
|
||||
}
|
||||
}
|
||||
if action.Runs != nil {
|
||||
entrypoint = action.Runs.Split()
|
|
@ -1,7 +1,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
|
@ -21,7 +20,7 @@ func drawGraph(plan *model.Plan) error {
|
|||
|
||||
ids := make([]string, 0)
|
||||
for _, r := range stage.Runs {
|
||||
ids = append(ids, fmt.Sprintf("%s/%s", r.Workflow.Name, r.JobID))
|
||||
ids = append(ids, r.String())
|
||||
}
|
||||
drawings = append(drawings, jobPen.DrawBoxes(ids...))
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
|
||||
// Input contains the input for the root command
|
||||
type Input struct {
|
||||
workingDir string
|
||||
workdir string
|
||||
workflowsPath string
|
||||
eventPath string
|
||||
reuseContainers bool
|
||||
|
@ -16,7 +16,7 @@ type Input struct {
|
|||
}
|
||||
|
||||
func (i *Input) resolve(path string) string {
|
||||
basedir, err := filepath.Abs(i.workingDir)
|
||||
basedir, err := filepath.Abs(i.workdir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -29,6 +29,11 @@ func (i *Input) resolve(path string) string {
|
|||
return path
|
||||
}
|
||||
|
||||
// Workdir returns path to workdir
|
||||
func (i *Input) Workdir() string {
|
||||
return i.resolve(".")
|
||||
}
|
||||
|
||||
// WorkflowsPath returns path to workflows
|
||||
func (i *Input) WorkflowsPath() string {
|
||||
return i.resolve(i.workflowsPath)
|
||||
|
|
43
cmd/root.go
43
cmd/root.go
|
@ -5,8 +5,11 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/nektos/act/pkg/common"
|
||||
|
||||
fswatch "github.com/andreaskoch/go-fswatch"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"github.com/nektos/act/pkg/runner"
|
||||
gitignore "github.com/sabhiram/go-gitignore"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -31,7 +34,7 @@ func Execute(ctx context.Context, version string) {
|
|||
rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", false, "pull docker image(s) if already present")
|
||||
rootCmd.Flags().StringVarP(&input.eventPath, "event", "e", "", "path to event JSON file")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow files")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.workingDir, "directory", "C", ".", "working directory")
|
||||
rootCmd.PersistentFlags().StringVarP(&input.workdir, "directory", "C", ".", "working directory")
|
||||
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
|
||||
rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "dryrun mode")
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
|
@ -87,26 +90,30 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
|
|||
}
|
||||
|
||||
// run the plan
|
||||
// runner, err := runner.New(config)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer runner.Close()
|
||||
config := &runner.Config{
|
||||
EventName: eventName,
|
||||
EventPath: input.EventPath(),
|
||||
ForcePull: input.forcePull,
|
||||
ReuseContainers: input.reuseContainers,
|
||||
Workdir: input.Workdir(),
|
||||
}
|
||||
runner, err := runner.New(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if watch, err := cmd.Flags().GetBool("watch"); err != nil {
|
||||
// return err
|
||||
// } else if watch {
|
||||
// return watchAndRun(ctx, func() error {
|
||||
// return runner.RunPlan(plan)
|
||||
// })
|
||||
// }
|
||||
ctx = common.WithDryrun(ctx, input.dryrun)
|
||||
if watch, err := cmd.Flags().GetBool("watch"); err != nil {
|
||||
return err
|
||||
} else if watch {
|
||||
return watchAndRun(ctx, runner.NewPlanExecutor(plan))
|
||||
}
|
||||
|
||||
// return runner.RunPlan(plan)
|
||||
return nil
|
||||
return runner.NewPlanExecutor(plan)(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func watchAndRun(ctx context.Context, fn func() error) error {
|
||||
func watchAndRun(ctx context.Context, fn common.Executor) error {
|
||||
recurse := true
|
||||
checkIntervalInSeconds := 2
|
||||
dir, err := os.Getwd()
|
||||
|
@ -132,13 +139,13 @@ func watchAndRun(ctx context.Context, fn func() error) error {
|
|||
|
||||
go func() {
|
||||
for folderWatcher.IsRunning() {
|
||||
if err = fn(); err != nil {
|
||||
if err = fn(ctx); err != nil {
|
||||
break
|
||||
}
|
||||
log.Debugf("Watching %s for changes", dir)
|
||||
for changes := range folderWatcher.ChangeDetails() {
|
||||
log.Debugf("%s", changes.String())
|
||||
if err = fn(); err != nil {
|
||||
if err = fn(ctx); err != nil {
|
||||
break
|
||||
}
|
||||
log.Debugf("Watching %s for changes", dir)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue