successfully able to run simple workflows

Signed-off-by: Casey Lee <cplee@nektos.com>
This commit is contained in:
Casey Lee 2020-02-06 22:17:58 -08:00
parent 8c49ba0cec
commit 532af98aef
No known key found for this signature in database
GPG key ID: 1899120ECD0A1784
23 changed files with 958 additions and 495 deletions

View file

@ -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

View file

@ -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()

View file

@ -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...))
}

View file

@ -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)

View file

@ -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)

9
go.mod
View file

@ -13,10 +13,8 @@ require (
github.com/emirpasic/gods v1.12.0 // indirect
github.com/go-ini/ini v1.41.0
github.com/gogo/protobuf v1.2.0 // indirect
github.com/gophercloud/gophercloud v0.7.0
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/gorilla/mux v1.7.0 // indirect
github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect
@ -24,7 +22,7 @@ require (
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/runc v0.1.1 // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/pkg/errors v0.8.1
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94
github.com/sirupsen/logrus v1.3.0
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
@ -33,9 +31,14 @@ require (
github.com/spf13/pflag v1.0.3 // indirect
github.com/stretchr/testify v1.3.0
golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect
golang.org/x/text v0.3.2 // indirect
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
google.golang.org/genproto v0.0.0-20190128161407-8ac453e89fca // indirect
google.golang.org/grpc v1.18.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/ini.v1 v1.41.0 // indirect
gopkg.in/src-d/go-billy.v4 v4.3.0 // indirect
gopkg.in/src-d/go-git-fixtures.v3 v3.3.0 // indirect

25
go.sum
View file

@ -45,14 +45,10 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gophercloud/gophercloud v0.7.0 h1:vhmQQEM2SbnGCg2/3EzQnQZ3V7+UCGy9s8exQCprNYg=
github.com/gophercloud/gophercloud v0.7.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U=
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93 h1:T1Q6ag9tCwun16AW+XK3tAql24P4uTGUMIn1/92WsQQ=
github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
@ -77,10 +73,6 @@ github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnG
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
@ -122,8 +114,6 @@ github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro
github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 h1:MQ/ZZiDsUapFFiMS+vzwXkCTeEKaum+Do5rINYJDmxc=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e h1:egKlR8l7Nu9vHGWbcUV8lqR4987UfUbBd7GbhqGzNYU=
golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -133,33 +123,24 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190201152629-afcc84fd7533 h1:bLfqnzrpeG4usq5OvMCrwTdmMJ6aTmlCuo1eKl0mhkI=
golang.org/x/sys v0.0.0-20190201152629-afcc84fd7533/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU=
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
@ -167,9 +148,6 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52 h1:JG/0uqcGdTNgq7FdU+61l5Pdmb8putNZlXb65bJBROs=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@ -197,7 +175,6 @@ gopkg.in/src-d/go-git.v4 v4.9.1 h1:0oKHJZY8tM7B71378cfTg2c5jmWyNlXvestTT6WfY+4=
gopkg.in/src-d/go-git.v4 v4.9.1/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=

25
pkg/common/dryrun.go Normal file
View 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)
}

View file

@ -1,6 +1,7 @@
package common
import (
"context"
"fmt"
log "github.com/sirupsen/logrus"
@ -25,26 +26,76 @@ 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 {
for _, executor := range executors {
if executor == nil {
continue
if executors == nil {
return func(ctx context.Context) error {
return nil
}
}
var rtn Executor
for _, executor := range executors {
if rtn == nil {
rtn = executor
} else {
rtn = rtn.Then(executor)
}
}
return rtn
}
// NewConditionalExecutor creates a new executor based on conditions
func NewConditionalExecutor(conditional Conditional, trueExecutor Executor, falseExecutor Executor) Executor {
return func(ctx context.Context) error {
if conditional(ctx) {
if trueExecutor != nil {
return trueExecutor(ctx)
}
err := executor()
if err != nil {
switch err.(type) {
case Warning:
log.Warning(err.Error())
return nil
default:
log.Debugf("%+v", err)
} else {
if falseExecutor != nil {
return falseExecutor(ctx)
}
}
return nil
}
}
// NewErrorExecutor creates a new executor that always errors out
func NewErrorExecutor(err error) Executor {
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(ctx context.Context) error {
errChan := make(chan error)
for _, executor := range executors {
go executor.ChannelError(errChan)(ctx)
}
for i := 0; i < len(executors); i++ {
select {
case <-ctx.Done():
return ctx.Err()
case err := <-errChan:
if err != nil {
return err
}
}
@ -53,48 +104,76 @@ func NewPipelineExecutor(executors ...Executor) Executor {
}
}
// NewConditionalExecutor creates a new executor based on conditions
func NewConditionalExecutor(conditional Conditional, trueExecutor Executor, falseExecutor Executor) Executor {
return func() error {
if conditional() {
if trueExecutor != nil {
return trueExecutor()
}
} else {
if falseExecutor != nil {
return falseExecutor()
// 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
}
}
func executeWithChan(executor Executor, errChan chan error) {
errChan <- executor()
// 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
}
}
// NewErrorExecutor creates a new executor that always errors out
func NewErrorExecutor(err error) Executor {
return func() error {
// 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
}
}
// NewParallelExecutor creates a new executor from a parallel of other executors
func NewParallelExecutor(executors ...Executor) Executor {
return func() error {
errChan := make(chan error)
for _, executor := range executors {
go executeWithChan(executor, errChan)
}
for i := 0; i < len(executors); i++ {
err := <-errChan
if err != nil {
return err
}
}
return nil
// Not return an inverted conditional
func (c Conditional) Not() Conditional {
return func(ctx context.Context) bool {
return !c(ctx)
}
}

View file

@ -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)

View file

@ -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
pkg/common/logger.go Normal file
View 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)
}

View file

@ -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
}

View file

@ -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
}

View 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...)
}
}

View file

@ -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
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
}

View file

@ -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,182 +30,198 @@ 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
}
}
// be sure to cleanup container if we aren't reusing
if !input.ReuseContainers {
defer removeContainer(input, cli, containerID)
}
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) {
isTerminal := terminal.IsTerminal(int(os.Stdout.Fd()))
config := &container.Config{
Image: input.Image,
Cmd: input.Cmd,
Entrypoint: input.Entrypoint,
WorkingDir: input.WorkingDir,
Env: input.Env,
Tty: isTerminal,
}
if len(input.Volumes) > 0 {
config.Volumes = make(map[string]struct{})
for _, vol := range input.Volumes {
config.Volumes[vol] = struct{}{}
}
}
resp, err := cli.ContainerCreate(input.Ctx, config, &container.HostConfig{
Binds: input.Binds,
}, nil, input.Name)
if err != nil {
return "", 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)
return resp.ID, nil
}
func findContainer(input NewDockerRunExecutorInput, cli *client.Client, containerName string) (string, error) {
containers, err := cli.ContainerList(input.Ctx, types.ContainerListOptions{
All: true,
})
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 nil
}
func attachContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error {
out, err := cli.ContainerAttach(input.Ctx, containerID, types.ContainerAttachOptions{
Stream: true,
Stdout: true,
Stderr: true,
})
if err != nil {
return err
}
isTerminal := terminal.IsTerminal(int(os.Stdout.Fd()))
if !isTerminal || os.Getenv("NORAW") != "" {
go input.logDockerOutput(out.Reader)
} else {
go input.streamDockerOutput(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)
return nil
}
func waitContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error {
statusCh, errCh := cli.ContainerWait(input.Ctx, containerID, container.WaitConditionNotRunning)
var statusCode int64
select {
case err := <-errCh:
if err != nil {
return err
}
case status := <-statusCh:
statusCode = status.StatusCode
}
input.Logger.Debugf("Return status: %v", statusCode)
if statusCode == 0 {
logger.Debugf("Removed container: %v", cr.id)
return nil
} else if statusCode == 78 {
return fmt.Errorf("exit with `NEUTRAL`: 78")
}
return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
}
func (cr *containerReference) create() common.Executor {
return func(ctx context.Context) error {
if cr.id != "" {
return nil
}
logger := common.Logger(ctx)
isTerminal := terminal.IsTerminal(int(os.Stdout.Fd()))
input := cr.input
config := &container.Config{
Image: input.Image,
Cmd: input.Cmd,
Entrypoint: input.Entrypoint,
WorkingDir: input.WorkingDir,
Env: input.Env,
Tty: isTerminal,
}
if len(input.Volumes) > 0 {
config.Volumes = make(map[string]struct{})
for _, vol := range input.Volumes {
config.Volumes[vol] = struct{}{}
}
}
resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{
Binds: input.Binds,
}, nil, input.Name)
if err != nil {
return errors.WithStack(err)
}
logger.Debugf("Created container name=%s id=%v from image %v", input.Name, resp.ID, input.Image)
logger.Debugf("ENV ==> %v", input.Env)
cr.id = resp.ID
return nil
}
}
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 errors.WithStack(err)
}
}
return nil
}
}
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 errors.WithStack(err)
}
isTerminal := terminal.IsTerminal(int(os.Stdout.Fd()))
if !isTerminal || os.Getenv("NORAW") != "" {
go logDockerOutput(ctx, out.Reader)
} else {
go streamDockerOutput(ctx, out.Reader)
}
return nil
}
}
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 (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 errors.WithStack(err)
}
case status := <-statusCh:
statusCode = status.StatusCode
}
logger.Debugf("Return status: %v", statusCode)
if statusCode == 0 {
return nil
} else if statusCode == 78 {
return fmt.Errorf("exit with `NEUTRAL`: 78")
}
return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
}
}

View file

@ -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)])
}

View file

@ -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()
}

View file

@ -1,7 +1,9 @@
package model
import (
"fmt"
"io"
"strings"
"gopkg.in/yaml.v2"
)
@ -16,13 +18,26 @@ type Workflow struct {
// Job is the structure of one job in a workflow
type Job struct {
Name string `yaml:"name"`
Needs []string `yaml:"needs"`
RunsOn string `yaml:"runs-on"`
Env map[string]string `yaml:"env"`
If string `yaml:"if"`
Steps []*Step `yaml:"steps"`
TimeoutMinutes int64 `yaml:"timeout-minutes"`
Name string `yaml:"name"`
Needs []string `yaml:"needs"`
RunsOn string `yaml:"runs-on"`
Env map[string]string `yaml:"env"`
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)

View file

@ -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
pkg/runner/run_context.go Normal file
View 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
}

View file

@ -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()
}