Compare commits

...

5 Commits

Author SHA1 Message Date
Jason Song
dca7801682 Support uses http(s)://host/owner/repo as actions (#14)
Examples:

```yaml
jobs:
  my_first_job:
    steps:
      - name: My first step
        uses: https://gitea.com/actions/heroku@main
      - name: My second step
        uses: http://example.com/actions/heroku@v2.0.1
```

Reviewed-on: https://gitea.com/gitea/act/pulls/14
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2023-02-15 16:28:33 +08:00
Lunny Xiao
4b99ed8916 Support go run on action (#12)
Reviewed-on: https://gitea.com/gitea/act/pulls/12
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-02-15 16:10:15 +08:00
Lunny Xiao
e46ede1b17 parse raw on (#11)
Reviewed-on: https://gitea.com/gitea/act/pulls/11
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-01-31 15:49:55 +08:00
Jason Song
1ba076d321 Erase needs of job in SingleWorkflow (#9)
Reviewed-on: https://gitea.com/gitea/act/pulls/9
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
2023-01-30 11:42:19 +08:00
appleboy
0efa2d5e63 fix(test): needs condition. (#8)
as title.

Signed-off-by: Bo-Yi.Wu <appleboy.tw@gmail.com>

Co-authored-by: Bo-Yi.Wu <appleboy.tw@gmail.com>
Reviewed-on: https://gitea.com/gitea/act/pulls/8
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-01-21 17:09:51 +08:00
8 changed files with 403 additions and 9 deletions

View File

@@ -16,7 +16,6 @@ func NewInterpeter(
gitCtx *model.GithubContext, gitCtx *model.GithubContext,
results map[string]*JobResult, results map[string]*JobResult,
) exprparser.Interpreter { ) exprparser.Interpreter {
strategy := make(map[string]interface{}) strategy := make(map[string]interface{})
if job.Strategy != nil { if job.Strategy != nil {
strategy["fail-fast"] = job.Strategy.FailFast strategy["fail-fast"] = job.Strategy.FailFast

View File

@@ -50,6 +50,7 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
runsOn[i] = evaluator.Interpolate(v) runsOn[i] = evaluator.Interpolate(v)
} }
job.RawRunsOn = encodeRunsOn(runsOn) job.RawRunsOn = encodeRunsOn(runsOn)
job.EraseNeeds() // there will be only one job in SingleWorkflow, it cannot have needs
ret = append(ret, &SingleWorkflow{ ret = append(ret, &SingleWorkflow{
Name: workflow.Name, Name: workflow.Name,
RawOn: workflow.RawOn, RawOn: workflow.RawOn,

View File

@@ -1,6 +1,8 @@
package jobparser package jobparser
import ( import (
"fmt"
"github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/model"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -121,3 +123,85 @@ type RunDefaults struct {
Shell string `yaml:"shell,omitempty"` Shell string `yaml:"shell,omitempty"`
WorkingDirectory string `yaml:"working-directory,omitempty"` WorkingDirectory string `yaml:"working-directory,omitempty"`
} }
type Event struct {
Name string
Acts map[string][]string
}
func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) {
switch rawOn.Kind {
case yaml.ScalarNode:
var val string
err := rawOn.Decode(&val)
if err != nil {
return nil, err
}
return []*Event{
{Name: val},
}, nil
case yaml.SequenceNode:
var val []interface{}
err := rawOn.Decode(&val)
if err != nil {
return nil, err
}
res := make([]*Event, 0, len(val))
for _, v := range val {
switch t := v.(type) {
case string:
res = append(res, &Event{Name: t})
default:
return nil, fmt.Errorf("invalid type %T", t)
}
}
return res, nil
case yaml.MappingNode:
var val map[string]interface{}
err := rawOn.Decode(&val)
if err != nil {
return nil, err
}
res := make([]*Event, 0, len(val))
for k, v := range val {
switch t := v.(type) {
case string:
res = append(res, &Event{
Name: k,
Acts: map[string][]string{},
})
case []string:
res = append(res, &Event{
Name: k,
Acts: map[string][]string{},
})
case map[string]interface{}:
acts := make(map[string][]string, len(t))
for act, branches := range t {
switch b := branches.(type) {
case string:
acts[act] = []string{b}
case []string:
acts[act] = b
case []interface{}:
acts[act] = make([]string, len(b))
for i, v := range b {
acts[act][i] = v.(string)
}
default:
return nil, fmt.Errorf("unknown on type: %#v", branches)
}
}
res = append(res, &Event{
Name: k,
Acts: acts,
})
default:
return nil, fmt.Errorf("unknown on type: %#v", v)
}
}
return res, nil
default:
return nil, fmt.Errorf("unknown on type: %v", rawOn.Kind)
}
}

184
pkg/jobparser/model_test.go Normal file
View File

@@ -0,0 +1,184 @@
package jobparser
import (
"fmt"
"strings"
"testing"
"github.com/nektos/act/pkg/model"
"github.com/stretchr/testify/assert"
)
func TestParseRawOn(t *testing.T) {
kases := []struct {
input string
result []*Event
}{
{
input: "on: issue_comment",
result: []*Event{
{
Name: "issue_comment",
},
},
},
{
input: "on:\n push",
result: []*Event{
{
Name: "push",
},
},
},
{
input: "on:\n - push\n - pull_request",
result: []*Event{
{
Name: "push",
},
{
Name: "pull_request",
},
},
},
{
input: "on:\n push:\n branches:\n - master",
result: []*Event{
{
Name: "push",
Acts: map[string][]string{
"branches": {
"master",
},
},
},
},
},
{
input: "on:\n branch_protection_rule:\n types: [created, deleted]",
result: []*Event{
{
Name: "branch_protection_rule",
Acts: map[string][]string{
"types": {
"created",
"deleted",
},
},
},
},
},
{
input: "on:\n project:\n types: [created, deleted]\n milestone:\n types: [opened, deleted]",
result: []*Event{
{
Name: "project",
Acts: map[string][]string{
"types": {
"created",
"deleted",
},
},
},
{
Name: "milestone",
Acts: map[string][]string{
"types": {
"opened",
"deleted",
},
},
},
},
},
{
input: "on:\n pull_request:\n types:\n - opened\n branches:\n - 'releases/**'",
result: []*Event{
{
Name: "pull_request",
Acts: map[string][]string{
"types": {
"opened",
},
"branches": {
"releases/**",
},
},
},
},
},
{
input: "on:\n push:\n branches:\n - main\n pull_request:\n types:\n - opened\n branches:\n - '**'",
result: []*Event{
{
Name: "push",
Acts: map[string][]string{
"branches": {
"main",
},
},
},
{
Name: "pull_request",
Acts: map[string][]string{
"types": {
"opened",
},
"branches": {
"**",
},
},
},
},
},
{
input: "on:\n push:\n branches:\n - 'main'\n - 'releases/**'",
result: []*Event{
{
Name: "push",
Acts: map[string][]string{
"branches": {
"main",
"releases/**",
},
},
},
},
},
{
input: "on:\n push:\n tags:\n - v1.**",
result: []*Event{
{
Name: "push",
Acts: map[string][]string{
"tags": {
"v1.**",
},
},
},
},
},
{
input: "on: [pull_request, workflow_dispatch]",
result: []*Event{
{
Name: "pull_request",
},
{
Name: "workflow_dispatch",
},
},
},
}
for _, kase := range kases {
t.Run(kase.input, func(t *testing.T) {
origin, err := model.ReadWorkflow(strings.NewReader(kase.input))
assert.NoError(t, err)
events, err := ParseRawOn(&origin.RawOn)
assert.NoError(t, err)
assert.EqualValues(t, kase.result, events, fmt.Sprintf("%#v", events))
})
}
}

View File

@@ -42,6 +42,8 @@ const (
ActionRunsUsingDocker = "docker" ActionRunsUsingDocker = "docker"
// ActionRunsUsingComposite for running composite // ActionRunsUsingComposite for running composite
ActionRunsUsingComposite = "composite" ActionRunsUsingComposite = "composite"
// ActionRunsUsingGo for running with go
ActionRunsUsingGo = "go"
) )
// ActionRuns are a field in Action // ActionRuns are a field in Action

View File

@@ -29,8 +29,10 @@ type actionStep interface {
type readAction func(ctx context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) type readAction func(ctx context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error)
type actionYamlReader func(filename string) (io.Reader, io.Closer, error) type (
type fileWriter func(filename string, data []byte, perm fs.FileMode) error actionYamlReader func(filename string) (io.Reader, io.Closer, error)
fileWriter func(filename string, data []byte, perm fs.FileMode) error
)
type runAction func(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor type runAction func(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor
@@ -61,7 +63,7 @@ func readActionImpl(ctx context.Context, step *model.Step, actionDir string, act
if b, err = trampoline.ReadFile("res/trampoline.js"); err != nil { if b, err = trampoline.ReadFile("res/trampoline.js"); err != nil {
return nil, err return nil, err
} }
err2 := writeFile(filepath.Join(actionDir, actionPath, "trampoline.js"), b, 0400) err2 := writeFile(filepath.Join(actionDir, actionPath, "trampoline.js"), b, 0o400)
if err2 != nil { if err2 != nil {
return nil, err2 return nil, err2
} }
@@ -167,6 +169,13 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
} }
return execAsComposite(step)(ctx) return execAsComposite(step)(ctx)
case model.ActionRunsUsingGo:
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
return err
}
containerArgs := []string{"go", "run", path.Join(containerActionDir, action.Runs.Main)}
logger.Debugf("executing remote job container: %s", containerArgs)
return rc.execJobContainer(containerArgs, *step.getEnv(), "", "")(ctx)
default: default:
return fmt.Errorf(fmt.Sprintf("The runs.using key must be one of: %v, got %s", []string{ return fmt.Errorf(fmt.Sprintf("The runs.using key must be one of: %v, got %s", []string{
model.ActionRunsUsingDocker, model.ActionRunsUsingDocker,

View File

@@ -44,8 +44,6 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
return fmt.Errorf("Expected format {org}/{repo}[/path]@ref. Actual '%s' Input string was not in a correct format", sar.Step.Uses) return fmt.Errorf("Expected format {org}/{repo}[/path]@ref. Actual '%s' Input string was not in a correct format", sar.Step.Uses)
} }
sar.remoteAction.URL = sar.RunContext.Config.DefaultActionInstance
github := sar.getGithubContext(ctx) github := sar.getGithubContext(ctx)
if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout { if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
common.Logger(ctx).Debugf("Skipping local actions/checkout because workdir was already copied") common.Logger(ctx).Debugf("Skipping local actions/checkout because workdir was already copied")
@@ -61,7 +59,7 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), strings.ReplaceAll(sar.Step.Uses, "/", "-")) actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), strings.ReplaceAll(sar.Step.Uses, "/", "-"))
gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{ gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{
URL: sar.remoteAction.CloneURL(), URL: sar.remoteAction.CloneURL(sar.RunContext.Config.DefaultActionInstance),
Ref: sar.remoteAction.Ref, Ref: sar.remoteAction.Ref,
Dir: actionDir, Dir: actionDir,
Token: "", /* Token: "", /*
@@ -215,8 +213,11 @@ type remoteAction struct {
Ref string Ref string
} }
func (ra *remoteAction) CloneURL() string { func (ra *remoteAction) CloneURL(defaultURL string) string {
u := ra.URL u := ra.URL
if u == "" {
u = defaultURL
}
if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") { if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
u = "https://" + u u = "https://" + u
} }
@@ -231,6 +232,26 @@ func (ra *remoteAction) IsCheckout() bool {
} }
func newRemoteAction(action string) *remoteAction { func newRemoteAction(action string) *remoteAction {
// support http(s)://host/owner/repo@v3
for _, schema := range []string{"https://", "http://"} {
if strings.HasPrefix(action, schema) {
splits := strings.SplitN(strings.TrimPrefix(action, schema), "/", 2)
if len(splits) != 2 {
return nil
}
ret := parseAction(splits[1])
if ret == nil {
return nil
}
ret.URL = schema + splits[0]
return ret
}
}
return parseAction(action)
}
func parseAction(action string) *remoteAction {
// GitHub's document[^] describes: // GitHub's document[^] describes:
// > We strongly recommend that you include the version of // > We strongly recommend that you include the version of
// > the action you are using by specifying a Git ref, SHA, or Docker tag number. // > the action you are using by specifying a Git ref, SHA, or Docker tag number.
@@ -246,6 +267,6 @@ func newRemoteAction(action string) *remoteAction {
Repo: matches[2], Repo: matches[2],
Path: matches[4], Path: matches[4],
Ref: matches[6], Ref: matches[6],
URL: "github.com", URL: "",
} }
} }

View File

@@ -623,3 +623,97 @@ func TestStepActionRemotePost(t *testing.T) {
}) })
} }
} }
func Test_newRemoteAction(t *testing.T) {
tests := []struct {
action string
want *remoteAction
wantCloneURL string
}{
{
action: "actions/heroku@main",
want: &remoteAction{
URL: "",
Org: "actions",
Repo: "heroku",
Path: "",
Ref: "main",
},
wantCloneURL: "https://github.com/actions/heroku",
},
{
action: "actions/aws/ec2@main",
want: &remoteAction{
URL: "",
Org: "actions",
Repo: "aws",
Path: "ec2",
Ref: "main",
},
wantCloneURL: "https://github.com/actions/aws",
},
{
action: "./.github/actions/my-action", // it's valid for GitHub, but act don't support it
want: nil,
},
{
action: "docker://alpine:3.8", // it's valid for GitHub, but act don't support it
want: nil,
},
{
action: "https://gitea.com/actions/heroku@main", // it's invalid for GitHub, but gitea supports it
want: &remoteAction{
URL: "https://gitea.com",
Org: "actions",
Repo: "heroku",
Path: "",
Ref: "main",
},
wantCloneURL: "https://gitea.com/actions/heroku",
},
{
action: "https://gitea.com/actions/aws/ec2@main", // it's invalid for GitHub, but gitea supports it
want: &remoteAction{
URL: "https://gitea.com",
Org: "actions",
Repo: "aws",
Path: "ec2",
Ref: "main",
},
wantCloneURL: "https://gitea.com/actions/aws",
},
{
action: "http://gitea.com/actions/heroku@main", // it's invalid for GitHub, but gitea supports it
want: &remoteAction{
URL: "http://gitea.com",
Org: "actions",
Repo: "heroku",
Path: "",
Ref: "main",
},
wantCloneURL: "http://gitea.com/actions/heroku",
},
{
action: "http://gitea.com/actions/aws/ec2@main", // it's invalid for GitHub, but gitea supports it
want: &remoteAction{
URL: "http://gitea.com",
Org: "actions",
Repo: "aws",
Path: "ec2",
Ref: "main",
},
wantCloneURL: "http://gitea.com/actions/aws",
},
}
for _, tt := range tests {
t.Run(tt.action, func(t *testing.T) {
got := newRemoteAction(tt.action)
assert.Equalf(t, tt.want, got, "newRemoteAction(%v)", tt.action)
cloneURL := ""
if got != nil {
cloneURL = got.CloneURL("github.com")
}
assert.Equalf(t, tt.wantCloneURL, cloneURL, "newRemoteAction(%v).CloneURL()", tt.action)
})
}
}