initial load of yaml working

This commit is contained in:
Casey Lee
2020-02-04 16:38:41 -08:00
parent 500e9677f3
commit 8c49ba0cec
30 changed files with 560 additions and 415 deletions

144
pkg/common/draw.go Normal file
View File

@@ -0,0 +1,144 @@
package common
import (
"fmt"
"io"
"os"
"strings"
)
// Style is a specific style
type Style int
// Styles
const (
StyleDoubleLine = iota
StyleSingleLine
StyleDashedLine
StyleNoLine
)
// NewPen creates a new pen
func NewPen(style Style, color int) *Pen {
bgcolor := 49
if os.Getenv("CLICOLOR") == "0" {
color = 0
bgcolor = 0
}
return &Pen{
style: style,
color: color,
bgcolor: bgcolor,
}
}
type styleDef struct {
cornerTL string
cornerTR string
cornerBL string
cornerBR string
lineH string
lineV string
}
var styleDefs = []styleDef{
{"\u2554", "\u2557", "\u255a", "\u255d", "\u2550", "\u2551"},
//{"\u250c", "\u2510", "\u2514", "\u2518", "\u2500", "\u2502"},
{"\u256d", "\u256e", "\u2570", "\u256f", "\u2500", "\u2502"},
{"\u250c", "\u2510", "\u2514", "\u2518", "\u254c", "\u254e"},
{" ", " ", " ", " ", " ", " "},
}
// Pen struct
type Pen struct {
style Style
color int
bgcolor int
}
// Drawing struct
type Drawing struct {
buf *strings.Builder
width int
}
func (p *Pen) drawTopBars(buf io.Writer, labels ...string) {
style := styleDefs[p.style]
for _, label := range labels {
bar := strings.Repeat(style.lineH, len(label)+2)
fmt.Fprintf(buf, " ")
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
fmt.Fprintf(buf, "%s%s%s", style.cornerTL, bar, style.cornerTR)
fmt.Fprintf(buf, "\x1b[%dm", 0)
}
fmt.Fprintf(buf, "\n")
}
func (p *Pen) drawBottomBars(buf io.Writer, labels ...string) {
style := styleDefs[p.style]
for _, label := range labels {
bar := strings.Repeat(style.lineH, len(label)+2)
fmt.Fprintf(buf, " ")
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
fmt.Fprintf(buf, "%s%s%s", style.cornerBL, bar, style.cornerBR)
fmt.Fprintf(buf, "\x1b[%dm", 0)
}
fmt.Fprintf(buf, "\n")
}
func (p *Pen) drawLabels(buf io.Writer, labels ...string) {
style := styleDefs[p.style]
for _, label := range labels {
fmt.Fprintf(buf, " ")
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
fmt.Fprintf(buf, "%s %s %s", style.lineV, label, style.lineV)
fmt.Fprintf(buf, "\x1b[%dm", 0)
}
fmt.Fprintf(buf, "\n")
}
// DrawArrow between boxes
func (p *Pen) DrawArrow() *Drawing {
drawing := &Drawing{
buf: new(strings.Builder),
width: 1,
}
fmt.Fprintf(drawing.buf, "\x1b[%dm", p.color)
fmt.Fprintf(drawing.buf, "\u2b07")
fmt.Fprintf(drawing.buf, "\x1b[%dm", 0)
return drawing
}
// DrawBoxes to draw boxes
func (p *Pen) DrawBoxes(labels ...string) *Drawing {
width := 0
for _, l := range labels {
width += len(l) + 2 + 2 + 1
}
drawing := &Drawing{
buf: new(strings.Builder),
width: width,
}
p.drawTopBars(drawing.buf, labels...)
p.drawLabels(drawing.buf, labels...)
p.drawBottomBars(drawing.buf, labels...)
return drawing
}
// Draw to writer
func (d *Drawing) Draw(writer io.Writer, centerOnWidth int) {
padSize := (centerOnWidth - d.GetWidth()) / 2
if padSize < 0 {
padSize = 0
}
for _, l := range strings.Split(d.buf.String(), "\n") {
if len(l) > 0 {
padding := strings.Repeat(" ", padSize)
fmt.Fprintf(writer, "%s%s\n", padding, l)
}
}
}
// GetWidth of drawing
func (d *Drawing) GetWidth() int {
return d.width
}

100
pkg/common/executor.go Normal file
View File

@@ -0,0 +1,100 @@
package common
import (
"fmt"
log "github.com/sirupsen/logrus"
)
// Warning that implements `error` but safe to ignore
type Warning struct {
Message string
}
// Error the contract for error
func (w Warning) Error() string {
return w.Message
}
// Warningf create a warning
func Warningf(format string, args ...interface{}) Warning {
w := Warning{
Message: fmt.Sprintf(format, args...),
}
return w
}
// Executor define contract for the steps of a workflow
type Executor func() error
// Conditional define contract for the conditional predicate
type Conditional func() bool
// 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
}
err := executor()
if err != nil {
switch err.(type) {
case Warning:
log.Warning(err.Error())
return nil
default:
log.Debugf("%+v", err)
return err
}
}
}
return nil
}
}
// 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()
}
}
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 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
}
}

View File

@@ -0,0 +1,84 @@
package common
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewWorkflow(t *testing.T) {
assert := assert.New(t)
// empty
emptyWorkflow := NewPipelineExecutor()
assert.Nil(emptyWorkflow())
// error case
errorWorkflow := NewErrorExecutor(fmt.Errorf("test error"))
assert.NotNil(errorWorkflow())
// multiple success case
runcount := 0
successWorkflow := NewPipelineExecutor(
func() error {
runcount++
return nil
},
func() error {
runcount++
return nil
})
assert.Nil(successWorkflow())
assert.Equal(2, runcount)
}
func TestNewConditionalExecutor(t *testing.T) {
assert := assert.New(t)
trueCount := 0
falseCount := 0
err := NewConditionalExecutor(func() bool {
return false
}, func() error {
trueCount++
return nil
}, func() error {
falseCount++
return nil
})()
assert.Nil(err)
assert.Equal(0, trueCount)
assert.Equal(1, falseCount)
err = NewConditionalExecutor(func() bool {
return true
}, func() error {
trueCount++
return nil
}, func() error {
falseCount++
return nil
})()
assert.Nil(err)
assert.Equal(1, trueCount)
assert.Equal(1, falseCount)
}
func TestNewParallelExecutor(t *testing.T) {
assert := assert.New(t)
count := 0
emptyWorkflow := NewPipelineExecutor(func() error {
count++
return nil
})
err := NewParallelExecutor(emptyWorkflow, emptyWorkflow)()
assert.Equal(2, count)
assert.Nil(err)
}

79
pkg/common/file.go Normal file
View File

@@ -0,0 +1,79 @@
package common
import (
"fmt"
"io"
"os"
)
// CopyFile copy file
func CopyFile(source string, dest string) (err error) {
sourcefile, err := os.Open(source)
if err != nil {
return err
}
defer sourcefile.Close()
destfile, err := os.Create(dest)
if err != nil {
return err
}
defer destfile.Close()
_, err = io.Copy(destfile, sourcefile)
if err == nil {
sourceinfo, err := os.Stat(source)
if err != nil {
_ = os.Chmod(dest, sourceinfo.Mode())
}
}
return
}
// CopyDir recursive copy of directory
func CopyDir(source string, dest string) (err error) {
// get properties of source dir
sourceinfo, err := os.Stat(source)
if err != nil {
return err
}
// create dest dir
err = os.MkdirAll(dest, sourceinfo.Mode())
if err != nil {
return err
}
directory, _ := os.Open(source)
objects, err := directory.Readdir(-1)
for _, obj := range objects {
sourcefilepointer := source + "/" + obj.Name()
destinationfilepointer := dest + "/" + obj.Name()
if obj.IsDir() {
// create sub-directories - recursively
err = CopyDir(sourcefilepointer, destinationfilepointer)
if err != nil {
fmt.Println(err)
}
} else {
// perform copy
err = CopyFile(sourcefilepointer, destinationfilepointer)
if err != nil {
fmt.Println(err)
}
}
}
return err
}

252
pkg/common/git.go Normal file
View File

@@ -0,0 +1,252 @@
package common
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"github.com/go-ini/ini"
log "github.com/sirupsen/logrus"
git "gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
)
var (
codeCommitHTTPRegex = regexp.MustCompile(`^https?://git-codecommit\.(.+)\.amazonaws.com/v1/repos/(.+)$`)
codeCommitSSHRegex = regexp.MustCompile(`ssh://git-codecommit\.(.+)\.amazonaws.com/v1/repos/(.+)$`)
githubHTTPRegex = regexp.MustCompile(`^https?://.*github.com.*/(.+)/(.+?)(?:.git)?$`)
githubSSHRegex = regexp.MustCompile(`github.com[:/](.+)/(.+).git$`)
cloneLock sync.Mutex
)
// FindGitRevision get the current git revision
func FindGitRevision(file string) (shortSha string, sha string, err error) {
gitDir, err := findGitDirectory(file)
if err != nil {
return "", "", err
}
bts, err := ioutil.ReadFile(filepath.Join(gitDir, "HEAD"))
if err != nil {
return "", "", err
}
var ref = strings.TrimSpace(strings.TrimPrefix(string(bts), "ref:"))
var refBuf []byte
if strings.HasPrefix(ref, "refs/") {
// load commitid ref
refBuf, err = ioutil.ReadFile(filepath.Join(gitDir, ref))
if err != nil {
return "", "", err
}
} else {
refBuf = []byte(ref)
}
log.Debugf("Found revision: %s", refBuf)
return string(refBuf[:7]), strings.TrimSpace(string(refBuf)), nil
}
// FindGitRef get the current git ref
func FindGitRef(file string) (string, error) {
gitDir, err := findGitDirectory(file)
if err != nil {
return "", err
}
log.Debugf("Loading revision from git directory '%s'", gitDir)
_, ref, err := FindGitRevision(file)
if err != nil {
return "", err
}
log.Debugf("HEAD points to '%s'", ref)
// try tags first
tag, err := findGitPrettyRef(ref, gitDir, "refs/tags")
if err != nil || tag != "" {
return tag, err
}
// and then branches
return findGitPrettyRef(ref, gitDir, "refs/heads")
}
func findGitPrettyRef(head, root, sub string) (string, error) {
var name string
var err = filepath.Walk(filepath.Join(root, sub), func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if name != "" {
return nil
}
if info.IsDir() {
return nil
}
bts, err := ioutil.ReadFile(path)
if err != nil {
return err
}
var pointsTo = strings.TrimSpace(string(bts))
if head == pointsTo {
name = strings.TrimPrefix(strings.Replace(path, root, "", 1), "/")
log.Debugf("HEAD matches %s", name)
}
return nil
})
return name, err
}
// FindGithubRepo get the repo
func FindGithubRepo(file string) (string, error) {
url, err := findGitRemoteURL(file)
if err != nil {
return "", err
}
_, slug, err := findGitSlug(url)
return slug, err
}
func findGitRemoteURL(file string) (string, error) {
gitDir, err := findGitDirectory(file)
if err != nil {
return "", err
}
log.Debugf("Loading slug from git directory '%s'", gitDir)
gitconfig, err := ini.InsensitiveLoad(fmt.Sprintf("%s/config", gitDir))
if err != nil {
return "", err
}
remote, err := gitconfig.GetSection("remote \"origin\"")
if err != nil {
return "", err
}
urlKey, err := remote.GetKey("url")
if err != nil {
return "", err
}
url := urlKey.String()
return url, nil
}
func findGitSlug(url string) (string, string, error) {
if matches := codeCommitHTTPRegex.FindStringSubmatch(url); matches != nil {
return "CodeCommit", matches[2], nil
} else if matches := codeCommitSSHRegex.FindStringSubmatch(url); matches != nil {
return "CodeCommit", matches[2], nil
} else if matches := githubHTTPRegex.FindStringSubmatch(url); matches != nil {
return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil
} else if matches := githubSSHRegex.FindStringSubmatch(url); matches != nil {
return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2]), nil
}
return "", url, nil
}
func findGitDirectory(fromFile string) (string, error) {
absPath, err := filepath.Abs(fromFile)
if err != nil {
return "", err
}
log.Debugf("Searching for git directory in %s", absPath)
fi, err := os.Stat(absPath)
if err != nil {
return "", err
}
var dir string
if fi.Mode().IsDir() {
dir = absPath
} else {
dir = filepath.Dir(absPath)
}
gitPath := filepath.Join(dir, ".git")
fi, err = os.Stat(gitPath)
if err == nil && fi.Mode().IsDir() {
return gitPath, nil
} else if dir == "/" || dir == "C:\\" || dir == "c:\\" {
return "", errors.New("unable to find git repo")
}
return findGitDirectory(filepath.Dir(dir))
}
// NewGitCloneExecutorInput the input for the NewGitCloneExecutor
type NewGitCloneExecutorInput struct {
URL string
Ref string
Dir string
Logger *log.Entry
Dryrun bool
}
// NewGitCloneExecutor creates an executor to clone git repos
func NewGitCloneExecutor(input NewGitCloneExecutorInput) Executor {
return func() error {
input.Logger.Infof("git clone '%s' # ref=%s", input.URL, input.Ref)
input.Logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
if input.Dryrun {
return nil
}
cloneLock.Lock()
defer cloneLock.Unlock()
refName := plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", input.Ref))
r, err := git.PlainOpen(input.Dir)
if err != nil {
r, err = git.PlainClone(input.Dir, false, &git.CloneOptions{
URL: input.URL,
Progress: input.Logger.WriterLevel(log.DebugLevel),
//ReferenceName: refName,
})
if err != nil {
input.Logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err)
return err
}
}
w, err := r.Worktree()
if err != nil {
return err
}
err = w.Pull(&git.PullOptions{
//ReferenceName: refName,
Force: true,
})
if err != nil && err.Error() != "already up-to-date" {
input.Logger.Errorf("Unable to pull %s: %v", refName, err)
}
input.Logger.Debugf("Cloned %s to %s", input.URL, input.Dir)
hash, err := r.ResolveRevision(plumbing.Revision(input.Ref))
if err != nil {
input.Logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
return err
}
err = w.Checkout(&git.CheckoutOptions{
//Branch: refName,
Hash: *hash,
Force: true,
})
if err != nil {
input.Logger.Errorf("Unable to checkout %s: %v", refName, err)
return err
}
input.Logger.Debugf("Checked out %s", input.Ref)
return nil
}
}

167
pkg/common/git_test.go Normal file
View File

@@ -0,0 +1,167 @@
package common
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"syscall"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFindGitSlug(t *testing.T) {
assert := assert.New(t)
var slugTests = []struct {
url string // input
provider string // expected result
slug string // expected result
}{
{"https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo-name", "CodeCommit", "my-repo-name"},
{"ssh://git-codecommit.us-west-2.amazonaws.com/v1/repos/my-repo", "CodeCommit", "my-repo"},
{"git@github.com:nektos/act.git", "GitHub", "nektos/act"},
{"https://github.com/nektos/act.git", "GitHub", "nektos/act"},
{"http://github.com/nektos/act.git", "GitHub", "nektos/act"},
{"https://github.com/nektos/act", "GitHub", "nektos/act"},
{"http://github.com/nektos/act", "GitHub", "nektos/act"},
{"git+ssh://git@github.com/owner/repo.git", "GitHub", "owner/repo"},
{"http://myotherrepo.com/act.git", "", "http://myotherrepo.com/act.git"},
}
for _, tt := range slugTests {
provider, slug, err := findGitSlug(tt.url)
assert.Nil(err)
assert.Equal(tt.provider, provider)
assert.Equal(tt.slug, slug)
}
}
func TestFindGitRemoteURL(t *testing.T) {
assert := assert.New(t)
basedir, err := ioutil.TempDir("", "act-test")
defer os.RemoveAll(basedir)
assert.Nil(err)
gitConfig()
err = gitCmd("init", basedir)
assert.Nil(err)
remoteURL := "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo-name"
err = gitCmd("config", "-f", fmt.Sprintf("%s/.git/config", basedir), "--add", "remote.origin.url", remoteURL)
assert.Nil(err)
u, err := findGitRemoteURL(basedir)
assert.Nil(err)
assert.Equal(remoteURL, u)
}
func TestGitFindRef(t *testing.T) {
basedir, err := ioutil.TempDir("", "act-test")
defer os.RemoveAll(basedir)
assert.NoError(t, err)
gitConfig()
for name, tt := range map[string]struct {
Prepare func(t *testing.T, dir string)
Assert func(t *testing.T, ref string, err error)
}{
"new_repo": {
Prepare: func(t *testing.T, dir string) {},
Assert: func(t *testing.T, ref string, err error) {
require.Error(t, err)
},
},
"new_repo_with_commit": {
Prepare: func(t *testing.T, dir string) {
require.NoError(t, gitCmd("-C", dir, "commit", "--allow-empty", "-m", "msg"))
},
Assert: func(t *testing.T, ref string, err error) {
require.NoError(t, err)
require.Equal(t, "refs/heads/master", ref)
},
},
"current_head_is_tag": {
Prepare: func(t *testing.T, dir string) {
require.NoError(t, gitCmd("-C", dir, "commit", "--allow-empty", "-m", "commit msg"))
require.NoError(t, gitCmd("-C", dir, "tag", "v1.2.3"))
require.NoError(t, gitCmd("-C", dir, "checkout", "v1.2.3"))
},
Assert: func(t *testing.T, ref string, err error) {
require.NoError(t, err)
require.Equal(t, "refs/tags/v1.2.3", ref)
},
},
"current_head_is_same_as_tag": {
Prepare: func(t *testing.T, dir string) {
require.NoError(t, gitCmd("-C", dir, "commit", "--allow-empty", "-m", "1.4.2 release"))
require.NoError(t, gitCmd("-C", dir, "tag", "v1.4.2"))
},
Assert: func(t *testing.T, ref string, err error) {
require.NoError(t, err)
require.Equal(t, "refs/tags/v1.4.2", ref)
},
},
"current_head_is_not_tag": {
Prepare: func(t *testing.T, dir string) {
require.NoError(t, gitCmd("-C", dir, "commit", "--allow-empty", "-m", "msg"))
require.NoError(t, gitCmd("-C", dir, "tag", "v1.4.2"))
require.NoError(t, gitCmd("-C", dir, "commit", "--allow-empty", "-m", "msg2"))
},
Assert: func(t *testing.T, ref string, err error) {
require.NoError(t, err)
require.Equal(t, "refs/heads/master", ref)
},
},
"current_head_is_another_branch": {
Prepare: func(t *testing.T, dir string) {
require.NoError(t, gitCmd("-C", dir, "checkout", "-b", "mybranch"))
require.NoError(t, gitCmd("-C", dir, "commit", "--allow-empty", "-m", "msg"))
},
Assert: func(t *testing.T, ref string, err error) {
require.NoError(t, err)
require.Equal(t, "refs/heads/mybranch", ref)
},
},
} {
tt := tt
name := name
t.Run(name, func(t *testing.T) {
dir := filepath.Join(basedir, name)
require.NoError(t, os.MkdirAll(dir, 0755))
require.NoError(t, gitCmd("-C", dir, "init"))
tt.Prepare(t, dir)
ref, err := FindGitRef(dir)
tt.Assert(t, ref, err)
})
}
}
func gitConfig() {
_ = gitCmd("config","--global","user.email","test@test.com")
_ = gitCmd("config","--global","user.name","Unit Test")
}
func gitCmd(args ...string) error {
cmd := exec.Command("git", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if exitError, ok := err.(*exec.ExitError); ok {
if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok {
return fmt.Errorf("Exit error %d", waitStatus.ExitStatus())
}
return exitError
}
return nil
}