Skip to content

Commit 2dd2d06

Browse files
lunnyChristopherHX
andcommitted
Move jobparser from act repository to Gitea (go-gitea#36699)
The jobparser sub package in act is only used by Gitea. Move it to Gitea to make it more easier to maintain. --------- Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
1 parent 159f740 commit 2dd2d06

37 files changed

Lines changed: 1925 additions & 18 deletions

.yamllint.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ rules:
2121
comments-indentation:
2222
level: error
2323

24-
document-start:
25-
level: error
26-
present: false
24+
document-start: disable
2725

2826
document-end:
2927
present: false

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ require (
118118
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
119119
github.com/yuin/goldmark-meta v1.1.0
120120
gitlab.com/gitlab-org/api/client-go v0.142.4
121+
go.yaml.in/yaml/v4 v4.0.0-rc.2
121122
golang.org/x/crypto v0.45.0
122123
golang.org/x/image v0.30.0
123124
golang.org/x/net v0.47.0
@@ -298,7 +299,7 @@ replace github.com/jaytaylor/html2text => github.com/Necoro/html2text v0.0.0-202
298299

299300
replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
300301

301-
replace github.com/nektos/act => gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763
302+
replace github.com/nektos/act => gitea.com/gitea/act v0.261.8
302303

303304
replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
304305

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
3131
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
3232
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
3333
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
34-
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c=
35-
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
34+
gitea.com/gitea/act v0.261.8 h1:rUWB5GOZOubfe2VteKb7XP3HRIbcW3UUmfh7bVAgQcA=
35+
gitea.com/gitea/act v0.261.8/go.mod h1:lTp4136rwbZiZS3ZVQeHCvd4qRAZ7LYeiRBqOSdMY/4=
3636
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
3737
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
3838
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
@@ -824,6 +824,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
824824
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
825825
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
826826
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
827+
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
828+
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
827829
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
828830
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
829831
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=

models/actions/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"code.gitea.io/gitea/models/db"
1515
repo_model "code.gitea.io/gitea/models/repo"
1616
user_model "code.gitea.io/gitea/models/user"
17+
"code.gitea.io/gitea/modules/actions/jobparser"
1718
"code.gitea.io/gitea/modules/git"
1819
"code.gitea.io/gitea/modules/json"
1920
"code.gitea.io/gitea/modules/setting"
@@ -22,7 +23,6 @@ import (
2223
"code.gitea.io/gitea/modules/util"
2324
webhook_module "code.gitea.io/gitea/modules/webhook"
2425

25-
"github.com/nektos/act/pkg/jobparser"
2626
"xorm.io/builder"
2727
)
2828

models/actions/task.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
auth_model "code.gitea.io/gitea/models/auth"
1414
"code.gitea.io/gitea/models/db"
1515
"code.gitea.io/gitea/models/unit"
16+
"code.gitea.io/gitea/modules/actions/jobparser"
1617
"code.gitea.io/gitea/modules/container"
1718
"code.gitea.io/gitea/modules/log"
1819
"code.gitea.io/gitea/modules/setting"
@@ -21,7 +22,6 @@ import (
2122

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

0 commit comments

Comments
 (0)