Skip to content

Commit e68d455

Browse files
committed
refactor(actions): use actionlint for event parsing
move jobparser in tree for quicker iteration
1 parent d030cac commit e68d455

33 files changed

+1837
-110
lines changed

models/actions/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ import (
1313
"code.gitea.io/gitea/models/db"
1414
repo_model "code.gitea.io/gitea/models/repo"
1515
user_model "code.gitea.io/gitea/models/user"
16+
"code.gitea.io/gitea/modules/actions/jobparser"
1617
"code.gitea.io/gitea/modules/git"
1718
"code.gitea.io/gitea/modules/json"
1819
api "code.gitea.io/gitea/modules/structs"
1920
"code.gitea.io/gitea/modules/timeutil"
2021
"code.gitea.io/gitea/modules/util"
2122
webhook_module "code.gitea.io/gitea/modules/webhook"
2223

23-
"github.com/nektos/act/pkg/jobparser"
2424
"xorm.io/builder"
2525
)
2626

models/actions/task.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import (
1919
"code.gitea.io/gitea/modules/util"
2020

2121
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
22+
"code.gitea.io/gitea/modules/actions/jobparser"
2223
lru "github.com/hashicorp/golang-lru/v2"
23-
"github.com/nektos/act/pkg/jobparser"
2424
"google.golang.org/protobuf/types/known/timestamppb"
2525
"xorm.io/builder"
2626
)

modules/actions/github.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ const (
2626
)
2727

2828
// IsDefaultBranchWorkflow returns true if the event only triggers workflows on the default branch
29-
func IsDefaultBranchWorkflow(triggedEvent webhook_module.HookEventType) bool {
30-
switch triggedEvent {
29+
func IsDefaultBranchWorkflow(triggeredEvent webhook_module.HookEventType) bool {
30+
switch triggeredEvent {
3131
case webhook_module.HookEventDelete:
3232
// GitHub "delete" event
3333
// https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#delete
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package jobparser
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
8+
"github.com/nektos/act/pkg/exprparser"
9+
"gopkg.in/yaml.v3"
10+
)
11+
12+
// ExpressionEvaluator is copied from runner.expressionEvaluator,
13+
// to avoid unnecessary dependencies
14+
type ExpressionEvaluator struct {
15+
interpreter exprparser.Interpreter
16+
}
17+
18+
func NewExpressionEvaluator(interpreter exprparser.Interpreter) *ExpressionEvaluator {
19+
return &ExpressionEvaluator{interpreter: interpreter}
20+
}
21+
22+
func (ee ExpressionEvaluator) evaluate(in string, defaultStatusCheck exprparser.DefaultStatusCheck) (interface{}, error) {
23+
evaluated, err := ee.interpreter.Evaluate(in, defaultStatusCheck)
24+
25+
return evaluated, err
26+
}
27+
28+
func (ee ExpressionEvaluator) evaluateScalarYamlNode(node *yaml.Node) error {
29+
var in string
30+
if err := node.Decode(&in); err != nil {
31+
return err
32+
}
33+
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
34+
return nil
35+
}
36+
expr, _ := rewriteSubExpression(in, false)
37+
res, err := ee.evaluate(expr, exprparser.DefaultStatusCheckNone)
38+
if err != nil {
39+
return err
40+
}
41+
return node.Encode(res)
42+
}
43+
44+
func (ee ExpressionEvaluator) evaluateMappingYamlNode(node *yaml.Node) error {
45+
// GitHub has this undocumented feature to merge maps, called insert directive
46+
insertDirective := regexp.MustCompile(`\${{\s*insert\s*}}`)
47+
for i := 0; i < len(node.Content)/2; {
48+
k := node.Content[i*2]
49+
v := node.Content[i*2+1]
50+
if err := ee.EvaluateYamlNode(v); err != nil {
51+
return err
52+
}
53+
var sk string
54+
// Merge the nested map of the insert directive
55+
if k.Decode(&sk) == nil && insertDirective.MatchString(sk) {
56+
node.Content = append(append(node.Content[:i*2], v.Content...), node.Content[(i+1)*2:]...)
57+
i += len(v.Content) / 2
58+
} else {
59+
if err := ee.EvaluateYamlNode(k); err != nil {
60+
return err
61+
}
62+
i++
63+
}
64+
}
65+
return nil
66+
}
67+
68+
func (ee ExpressionEvaluator) evaluateSequenceYamlNode(node *yaml.Node) error {
69+
for i := 0; i < len(node.Content); {
70+
v := node.Content[i]
71+
// Preserve nested sequences
72+
wasseq := v.Kind == yaml.SequenceNode
73+
if err := ee.EvaluateYamlNode(v); err != nil {
74+
return err
75+
}
76+
// GitHub has this undocumented feature to merge sequences / arrays
77+
// We have a nested sequence via evaluation, merge the arrays
78+
if v.Kind == yaml.SequenceNode && !wasseq {
79+
node.Content = append(append(node.Content[:i], v.Content...), node.Content[i+1:]...)
80+
i += len(v.Content)
81+
} else {
82+
i++
83+
}
84+
}
85+
return nil
86+
}
87+
88+
func (ee ExpressionEvaluator) EvaluateYamlNode(node *yaml.Node) error {
89+
switch node.Kind {
90+
case yaml.ScalarNode:
91+
return ee.evaluateScalarYamlNode(node)
92+
case yaml.MappingNode:
93+
return ee.evaluateMappingYamlNode(node)
94+
case yaml.SequenceNode:
95+
return ee.evaluateSequenceYamlNode(node)
96+
default:
97+
return nil
98+
}
99+
}
100+
101+
func (ee ExpressionEvaluator) Interpolate(in string) string {
102+
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
103+
return in
104+
}
105+
106+
expr, _ := rewriteSubExpression(in, true)
107+
evaluated, err := ee.evaluate(expr, exprparser.DefaultStatusCheckNone)
108+
if err != nil {
109+
return ""
110+
}
111+
112+
value, ok := evaluated.(string)
113+
if !ok {
114+
panic(fmt.Sprintf("Expression %s did not evaluate to a string", expr))
115+
}
116+
117+
return value
118+
}
119+
120+
func escapeFormatString(in string) string {
121+
return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
122+
}
123+
124+
func rewriteSubExpression(in string, forceFormat bool) (string, error) {
125+
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
126+
return in, nil
127+
}
128+
129+
strPattern := regexp.MustCompile("(?:''|[^'])*'")
130+
pos := 0
131+
exprStart := -1
132+
strStart := -1
133+
var results []string
134+
formatOut := ""
135+
for pos < len(in) {
136+
if strStart > -1 {
137+
matches := strPattern.FindStringIndex(in[pos:])
138+
if matches == nil {
139+
panic("unclosed string.")
140+
}
141+
142+
strStart = -1
143+
pos += matches[1]
144+
} else if exprStart > -1 {
145+
exprEnd := strings.Index(in[pos:], "}}")
146+
strStart = strings.Index(in[pos:], "'")
147+
148+
if exprEnd > -1 && strStart > -1 {
149+
if exprEnd < strStart {
150+
strStart = -1
151+
} else {
152+
exprEnd = -1
153+
}
154+
}
155+
156+
if exprEnd > -1 {
157+
formatOut += fmt.Sprintf("{%d}", len(results))
158+
results = append(results, strings.TrimSpace(in[exprStart:pos+exprEnd]))
159+
pos += exprEnd + 2
160+
exprStart = -1
161+
} else if strStart > -1 {
162+
pos += strStart + 1
163+
} else {
164+
panic("unclosed expression.")
165+
}
166+
} else {
167+
exprStart = strings.Index(in[pos:], "${{")
168+
if exprStart != -1 {
169+
formatOut += escapeFormatString(in[pos : pos+exprStart])
170+
exprStart = pos + exprStart + 3
171+
pos = exprStart
172+
} else {
173+
formatOut += escapeFormatString(in[pos:])
174+
pos = len(in)
175+
}
176+
}
177+
}
178+
179+
if len(results) == 1 && formatOut == "{0}" && !forceFormat {
180+
return in, nil
181+
}
182+
183+
out := fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut, "'", "''"), strings.Join(results, ", "))
184+
return out, nil
185+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package jobparser
2+
3+
import (
4+
"github.com/nektos/act/pkg/exprparser"
5+
"github.com/nektos/act/pkg/model"
6+
"gopkg.in/yaml.v3"
7+
)
8+
9+
// NewInterpeter returns an interpeter used in the server,
10+
// need github, needs, strategy, matrix, inputs context only,
11+
// see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
12+
func NewInterpeter(
13+
jobID string,
14+
job *model.Job,
15+
matrix map[string]interface{},
16+
gitCtx *model.GithubContext,
17+
results map[string]*JobResult,
18+
vars map[string]string,
19+
) exprparser.Interpreter {
20+
strategy := make(map[string]interface{})
21+
if job.Strategy != nil {
22+
strategy["fail-fast"] = job.Strategy.FailFast
23+
strategy["max-parallel"] = job.Strategy.MaxParallel
24+
}
25+
26+
run := &model.Run{
27+
Workflow: &model.Workflow{
28+
Jobs: map[string]*model.Job{},
29+
},
30+
JobID: jobID,
31+
}
32+
for id, result := range results {
33+
need := yaml.Node{}
34+
_ = need.Encode(result.Needs)
35+
run.Workflow.Jobs[id] = &model.Job{
36+
RawNeeds: need,
37+
Result: result.Result,
38+
Outputs: result.Outputs,
39+
}
40+
}
41+
42+
jobs := run.Workflow.Jobs
43+
jobNeeds := run.Job().Needs()
44+
45+
using := map[string]exprparser.Needs{}
46+
for _, need := range jobNeeds {
47+
if v, ok := jobs[need]; ok {
48+
using[need] = exprparser.Needs{
49+
Outputs: v.Outputs,
50+
Result: v.Result,
51+
}
52+
}
53+
}
54+
55+
ee := &exprparser.EvaluationEnvironment{
56+
Github: gitCtx,
57+
Env: nil, // no need
58+
Job: nil, // no need
59+
Steps: nil, // no need
60+
Runner: nil, // no need
61+
Secrets: nil, // no need
62+
Strategy: strategy,
63+
Matrix: matrix,
64+
Needs: using,
65+
Inputs: nil, // not supported yet
66+
Vars: vars,
67+
}
68+
69+
config := exprparser.Config{
70+
Run: run,
71+
WorkingDir: "", // WorkingDir is used for the function hashFiles, but it's not needed in the server
72+
Context: "job",
73+
}
74+
75+
return exprparser.NewInterpeter(ee, config)
76+
}
77+
78+
// JobResult is the minimum requirement of job results for Interpeter
79+
type JobResult struct {
80+
Needs []string
81+
Result string
82+
Outputs map[string]string
83+
}

0 commit comments

Comments
 (0)