mirror of
https://github.com/go-gitea/gitea.git
synced 2026-03-17 14:24:07 +00:00
Move jobparser from act repository to 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>
This commit is contained in:
@@ -21,9 +21,7 @@ rules:
|
|||||||
comments-indentation:
|
comments-indentation:
|
||||||
level: error
|
level: error
|
||||||
|
|
||||||
document-start:
|
document-start: disable
|
||||||
level: error
|
|
||||||
present: false
|
|
||||||
|
|
||||||
document-end:
|
document-end:
|
||||||
present: false
|
present: false
|
||||||
|
|||||||
5
assets/go-licenses.json
generated
5
assets/go-licenses.json
generated
@@ -1249,6 +1249,11 @@
|
|||||||
"path": "go.yaml.in/yaml/v3/LICENSE",
|
"path": "go.yaml.in/yaml/v3/LICENSE",
|
||||||
"licenseText": "\nThis project is covered by two different licenses: MIT and Apache.\n\n#### MIT License ####\n\nThe following files were ported to Go from C files of libyaml, and thus\nare still covered by their original MIT license, with the additional\ncopyright staring in 2011 when the project was ported over:\n\n apic.go emitterc.go parserc.go readerc.go scannerc.go\n writerc.go yamlh.go yamlprivateh.go\n\nCopyright (c) 2006-2010 Kirill Simonov\nCopyright (c) 2006-2011 Kirill Simonov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n### Apache License ###\n\nAll the remaining project files are covered by the Apache license:\n\nCopyright (c) 2011-2019 Canonical Ltd\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
|
"licenseText": "\nThis project is covered by two different licenses: MIT and Apache.\n\n#### MIT License ####\n\nThe following files were ported to Go from C files of libyaml, and thus\nare still covered by their original MIT license, with the additional\ncopyright staring in 2011 when the project was ported over:\n\n apic.go emitterc.go parserc.go readerc.go scannerc.go\n writerc.go yamlh.go yamlprivateh.go\n\nCopyright (c) 2006-2010 Kirill Simonov\nCopyright (c) 2006-2011 Kirill Simonov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n### Apache License ###\n\nAll the remaining project files are covered by the Apache license:\n\nCopyright (c) 2011-2019 Canonical Ltd\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "go.yaml.in/yaml/v4",
|
||||||
|
"path": "go.yaml.in/yaml/v4/LICENSE",
|
||||||
|
"licenseText": "\nThis project is covered by two different licenses: MIT and Apache.\n\n#### MIT License ####\n\nThe following files were ported to Go from C files of libyaml, and thus\nare still covered by their original MIT license, with the additional\ncopyright staring in 2011 when the project was ported over:\n\n apic.go emitterc.go parserc.go readerc.go scannerc.go\n writerc.go yamlh.go yamlprivateh.go\n\nCopyright (c) 2006-2010 Kirill Simonov\nCopyright (c) 2006-2011 Kirill Simonov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n### Apache License ###\n\nAll the remaining project files are covered by the Apache license:\n\nCopyright (c) 2011-2019 Canonical Ltd\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "go4.org",
|
"name": "go4.org",
|
||||||
"path": "go4.org/LICENSE",
|
"path": "go4.org/LICENSE",
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -112,6 +112,7 @@ require (
|
|||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
github.com/yuin/goldmark-meta v1.1.0
|
github.com/yuin/goldmark-meta v1.1.0
|
||||||
gitlab.com/gitlab-org/api/client-go v0.142.4
|
gitlab.com/gitlab-org/api/client-go v0.142.4
|
||||||
|
go.yaml.in/yaml/v4 v4.0.0-rc.2
|
||||||
golang.org/x/crypto v0.47.0
|
golang.org/x/crypto v0.47.0
|
||||||
golang.org/x/image v0.35.0
|
golang.org/x/image v0.35.0
|
||||||
golang.org/x/net v0.49.0
|
golang.org/x/net v0.49.0
|
||||||
@@ -294,7 +295,7 @@ ignore (
|
|||||||
|
|
||||||
replace github.com/jaytaylor/html2text => github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347
|
replace github.com/jaytaylor/html2text => github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347
|
||||||
|
|
||||||
replace github.com/nektos/act => gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763
|
replace github.com/nektos/act => gitea.com/gitea/act v0.261.8
|
||||||
|
|
||||||
replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
|
replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
|
||||||
|
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -31,8 +31,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
|||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c=
|
gitea.com/gitea/act v0.261.8 h1:rUWB5GOZOubfe2VteKb7XP3HRIbcW3UUmfh7bVAgQcA=
|
||||||
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
gitea.com/gitea/act v0.261.8/go.mod h1:lTp4136rwbZiZS3ZVQeHCvd4qRAZ7LYeiRBqOSdMY/4=
|
||||||
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
|
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
|
||||||
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
|
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
|
||||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
|
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
|
||||||
@@ -829,6 +829,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
|||||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
|
||||||
|
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/modules/actions/jobparser"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/jobparser"
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
|
"code.gitea.io/gitea/modules/actions/jobparser"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
@@ -21,7 +22,6 @@ import (
|
|||||||
|
|
||||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
lru "github.com/hashicorp/golang-lru/v2"
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
"github.com/nektos/act/pkg/jobparser"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/jobparser"
|
"code.gitea.io/gitea/modules/actions/jobparser"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
189
modules/actions/jobparser/evaluator.go
Normal file
189
modules/actions/jobparser/evaluator.go
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package jobparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/exprparser"
|
||||||
|
"go.yaml.in/yaml/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExpressionEvaluator is copied from runner.expressionEvaluator,
|
||||||
|
// to avoid unnecessary dependencies
|
||||||
|
type ExpressionEvaluator struct {
|
||||||
|
interpreter exprparser.Interpreter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExpressionEvaluator(interpreter exprparser.Interpreter) *ExpressionEvaluator {
|
||||||
|
return &ExpressionEvaluator{interpreter: interpreter}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ee ExpressionEvaluator) evaluate(in string, defaultStatusCheck exprparser.DefaultStatusCheck) (any, error) {
|
||||||
|
evaluated, err := ee.interpreter.Evaluate(in, defaultStatusCheck)
|
||||||
|
|
||||||
|
return evaluated, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ee ExpressionEvaluator) evaluateScalarYamlNode(node *yaml.Node) error {
|
||||||
|
var in string
|
||||||
|
if err := node.Decode(&in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
expr, _ := rewriteSubExpression(in, false)
|
||||||
|
res, err := ee.evaluate(expr, exprparser.DefaultStatusCheckNone)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return node.Encode(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ee ExpressionEvaluator) evaluateMappingYamlNode(node *yaml.Node) error {
|
||||||
|
// GitHub has this undocumented feature to merge maps, called insert directive
|
||||||
|
insertDirective := regexp.MustCompile(`\${{\s*insert\s*}}`)
|
||||||
|
for i := 0; i < len(node.Content)/2; {
|
||||||
|
k := node.Content[i*2]
|
||||||
|
v := node.Content[i*2+1]
|
||||||
|
if err := ee.EvaluateYamlNode(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var sk string
|
||||||
|
// Merge the nested map of the insert directive
|
||||||
|
if k.Decode(&sk) == nil && insertDirective.MatchString(sk) {
|
||||||
|
node.Content = append(append(node.Content[:i*2], v.Content...), node.Content[(i+1)*2:]...)
|
||||||
|
i += len(v.Content) / 2
|
||||||
|
} else {
|
||||||
|
if err := ee.EvaluateYamlNode(k); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ee ExpressionEvaluator) evaluateSequenceYamlNode(node *yaml.Node) error {
|
||||||
|
for i := 0; i < len(node.Content); {
|
||||||
|
v := node.Content[i]
|
||||||
|
// Preserve nested sequences
|
||||||
|
wasseq := v.Kind == yaml.SequenceNode
|
||||||
|
if err := ee.EvaluateYamlNode(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// GitHub has this undocumented feature to merge sequences / arrays
|
||||||
|
// We have a nested sequence via evaluation, merge the arrays
|
||||||
|
if v.Kind == yaml.SequenceNode && !wasseq {
|
||||||
|
node.Content = append(append(node.Content[:i], v.Content...), node.Content[i+1:]...)
|
||||||
|
i += len(v.Content)
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ee ExpressionEvaluator) EvaluateYamlNode(node *yaml.Node) error {
|
||||||
|
switch node.Kind {
|
||||||
|
case yaml.ScalarNode:
|
||||||
|
return ee.evaluateScalarYamlNode(node)
|
||||||
|
case yaml.MappingNode:
|
||||||
|
return ee.evaluateMappingYamlNode(node)
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
return ee.evaluateSequenceYamlNode(node)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ee ExpressionEvaluator) Interpolate(in string) string {
|
||||||
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
expr, _ := rewriteSubExpression(in, true)
|
||||||
|
evaluated, err := ee.evaluate(expr, exprparser.DefaultStatusCheckNone)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
value, ok := evaluated.(string)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("Expression %s did not evaluate to a string", expr))
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeFormatString(in string) string {
|
||||||
|
return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func rewriteSubExpression(in string, forceFormat bool) (string, error) {
|
||||||
|
if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") {
|
||||||
|
return in, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
strPattern := regexp.MustCompile("(?:''|[^'])*'")
|
||||||
|
pos := 0
|
||||||
|
exprStart := -1
|
||||||
|
strStart := -1
|
||||||
|
var results []string
|
||||||
|
formatOut := ""
|
||||||
|
for pos < len(in) {
|
||||||
|
if strStart > -1 {
|
||||||
|
matches := strPattern.FindStringIndex(in[pos:])
|
||||||
|
if matches == nil {
|
||||||
|
return "", errors.New("unclosed string")
|
||||||
|
}
|
||||||
|
|
||||||
|
strStart = -1
|
||||||
|
pos += matches[1]
|
||||||
|
} else if exprStart > -1 {
|
||||||
|
exprEnd := strings.Index(in[pos:], "}}")
|
||||||
|
strStart = strings.Index(in[pos:], "'")
|
||||||
|
|
||||||
|
if exprEnd > -1 && strStart > -1 {
|
||||||
|
if exprEnd < strStart {
|
||||||
|
strStart = -1
|
||||||
|
} else {
|
||||||
|
exprEnd = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if exprEnd > -1 {
|
||||||
|
formatOut += fmt.Sprintf("{%d}", len(results))
|
||||||
|
results = append(results, strings.TrimSpace(in[exprStart:pos+exprEnd]))
|
||||||
|
pos += exprEnd + 2
|
||||||
|
exprStart = -1
|
||||||
|
} else if strStart > -1 {
|
||||||
|
pos += strStart + 1
|
||||||
|
} else {
|
||||||
|
panic("unclosed expression.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exprStart = strings.Index(in[pos:], "${{")
|
||||||
|
if exprStart != -1 {
|
||||||
|
formatOut += escapeFormatString(in[pos : pos+exprStart])
|
||||||
|
exprStart = pos + exprStart + 3
|
||||||
|
pos = exprStart
|
||||||
|
} else {
|
||||||
|
formatOut += escapeFormatString(in[pos:])
|
||||||
|
pos = len(in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 1 && formatOut == "{0}" && !forceFormat {
|
||||||
|
return in, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut, "'", "''"), strings.Join(results, ", "))
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
87
modules/actions/jobparser/interpeter.go
Normal file
87
modules/actions/jobparser/interpeter.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package jobparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nektos/act/pkg/exprparser"
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"go.yaml.in/yaml/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewInterpeter returns an interpeter used in the server,
|
||||||
|
// need github, needs, strategy, matrix, inputs context only,
|
||||||
|
// see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
|
||||||
|
func NewInterpeter(
|
||||||
|
jobID string,
|
||||||
|
job *model.Job,
|
||||||
|
matrix map[string]any,
|
||||||
|
gitCtx *model.GithubContext,
|
||||||
|
results map[string]*JobResult,
|
||||||
|
vars map[string]string,
|
||||||
|
inputs map[string]any,
|
||||||
|
) exprparser.Interpreter {
|
||||||
|
strategy := make(map[string]any)
|
||||||
|
if job.Strategy != nil {
|
||||||
|
strategy["fail-fast"] = job.Strategy.FailFast
|
||||||
|
strategy["max-parallel"] = job.Strategy.MaxParallel
|
||||||
|
}
|
||||||
|
|
||||||
|
run := &model.Run{
|
||||||
|
Workflow: &model.Workflow{
|
||||||
|
Jobs: map[string]*model.Job{},
|
||||||
|
},
|
||||||
|
JobID: jobID,
|
||||||
|
}
|
||||||
|
for id, result := range results {
|
||||||
|
need := yaml.Node{}
|
||||||
|
_ = need.Encode(result.Needs)
|
||||||
|
run.Workflow.Jobs[id] = &model.Job{
|
||||||
|
RawNeeds: need,
|
||||||
|
Result: result.Result,
|
||||||
|
Outputs: result.Outputs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs := run.Workflow.Jobs
|
||||||
|
jobNeeds := run.Job().Needs()
|
||||||
|
|
||||||
|
using := map[string]exprparser.Needs{}
|
||||||
|
for _, need := range jobNeeds {
|
||||||
|
if v, ok := jobs[need]; ok {
|
||||||
|
using[need] = exprparser.Needs{
|
||||||
|
Outputs: v.Outputs,
|
||||||
|
Result: v.Result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ee := &exprparser.EvaluationEnvironment{
|
||||||
|
Github: gitCtx,
|
||||||
|
Env: nil, // no need
|
||||||
|
Job: nil, // no need
|
||||||
|
Steps: nil, // no need
|
||||||
|
Runner: nil, // no need
|
||||||
|
Secrets: nil, // no need
|
||||||
|
Strategy: strategy,
|
||||||
|
Matrix: matrix,
|
||||||
|
Needs: using,
|
||||||
|
Inputs: inputs,
|
||||||
|
Vars: vars,
|
||||||
|
}
|
||||||
|
|
||||||
|
config := exprparser.Config{
|
||||||
|
Run: run,
|
||||||
|
WorkingDir: "", // WorkingDir is used for the function hashFiles, but it's not needed in the server
|
||||||
|
Context: "job",
|
||||||
|
}
|
||||||
|
|
||||||
|
return exprparser.NewInterpeter(ee, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JobResult is the minimum requirement of job results for Interpeter
|
||||||
|
type JobResult struct {
|
||||||
|
Needs []string
|
||||||
|
Result string
|
||||||
|
Outputs map[string]string
|
||||||
|
}
|
||||||
177
modules/actions/jobparser/jobparser.go
Normal file
177
modules/actions/jobparser/jobparser.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package jobparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/exprparser"
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"go.yaml.in/yaml/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) {
|
||||||
|
origin, err := model.ReadWorkflow(bytes.NewReader(content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("model.ReadWorkflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow := &SingleWorkflow{}
|
||||||
|
if err := yaml.Unmarshal(content, workflow); err != nil {
|
||||||
|
return nil, fmt.Errorf("yaml.Unmarshal: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pc := &parseContext{}
|
||||||
|
for _, o := range options {
|
||||||
|
o(pc)
|
||||||
|
}
|
||||||
|
results := map[string]*JobResult{}
|
||||||
|
for id, job := range origin.Jobs {
|
||||||
|
results[id] = &JobResult{
|
||||||
|
Needs: job.Needs(),
|
||||||
|
Result: pc.jobResults[id],
|
||||||
|
Outputs: nil, // not supported yet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret []*SingleWorkflow
|
||||||
|
ids, jobs, err := workflow.jobs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid jobs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluator := NewExpressionEvaluator(exprparser.NewInterpeter(&exprparser.EvaluationEnvironment{Github: pc.gitContext, Vars: pc.vars, Inputs: pc.inputs}, exprparser.Config{}))
|
||||||
|
workflow.RunName = evaluator.Interpolate(workflow.RunName)
|
||||||
|
|
||||||
|
for i, id := range ids {
|
||||||
|
job := jobs[i]
|
||||||
|
matricxes, err := getMatrixes(origin.GetJob(id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getMatrixes: %w", err)
|
||||||
|
}
|
||||||
|
for _, matrix := range matricxes {
|
||||||
|
job := job.Clone()
|
||||||
|
if job.Name == "" {
|
||||||
|
job.Name = id
|
||||||
|
}
|
||||||
|
job.Strategy.RawMatrix = encodeMatrix(matrix)
|
||||||
|
evaluator := NewExpressionEvaluator(NewInterpeter(id, origin.GetJob(id), matrix, pc.gitContext, results, pc.vars, pc.inputs))
|
||||||
|
job.Name = nameWithMatrix(job.Name, matrix, evaluator)
|
||||||
|
runsOn := origin.GetJob(id).RunsOn()
|
||||||
|
for i, v := range runsOn {
|
||||||
|
runsOn[i] = evaluator.Interpolate(v)
|
||||||
|
}
|
||||||
|
job.RawRunsOn = encodeRunsOn(runsOn)
|
||||||
|
swf := &SingleWorkflow{
|
||||||
|
Name: workflow.Name,
|
||||||
|
RawOn: workflow.RawOn,
|
||||||
|
Env: workflow.Env,
|
||||||
|
Defaults: workflow.Defaults,
|
||||||
|
RawPermissions: workflow.RawPermissions,
|
||||||
|
RunName: workflow.RunName,
|
||||||
|
}
|
||||||
|
if err := swf.SetJob(id, job); err != nil {
|
||||||
|
return nil, fmt.Errorf("SetJob: %w", err)
|
||||||
|
}
|
||||||
|
ret = append(ret, swf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithJobResults(results map[string]string) ParseOption {
|
||||||
|
return func(c *parseContext) {
|
||||||
|
c.jobResults = results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithGitContext(context *model.GithubContext) ParseOption {
|
||||||
|
return func(c *parseContext) {
|
||||||
|
c.gitContext = context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithVars(vars map[string]string) ParseOption {
|
||||||
|
return func(c *parseContext) {
|
||||||
|
c.vars = vars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithInputs(inputs map[string]any) ParseOption {
|
||||||
|
return func(c *parseContext) {
|
||||||
|
c.inputs = inputs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type parseContext struct {
|
||||||
|
jobResults map[string]string
|
||||||
|
gitContext *model.GithubContext
|
||||||
|
vars map[string]string
|
||||||
|
inputs map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParseOption func(c *parseContext)
|
||||||
|
|
||||||
|
func getMatrixes(job *model.Job) ([]map[string]any, error) {
|
||||||
|
ret, err := job.GetMatrixes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GetMatrixes: %w", err)
|
||||||
|
}
|
||||||
|
sort.Slice(ret, func(i, j int) bool {
|
||||||
|
return matrixName(ret[i]) < matrixName(ret[j])
|
||||||
|
})
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeMatrix(matrix map[string]any) yaml.Node {
|
||||||
|
if len(matrix) == 0 {
|
||||||
|
return yaml.Node{}
|
||||||
|
}
|
||||||
|
value := map[string][]any{}
|
||||||
|
for k, v := range matrix {
|
||||||
|
value[k] = []any{v}
|
||||||
|
}
|
||||||
|
node := yaml.Node{}
|
||||||
|
_ = node.Encode(value)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeRunsOn(runsOn []string) yaml.Node {
|
||||||
|
node := yaml.Node{}
|
||||||
|
if len(runsOn) == 1 {
|
||||||
|
_ = node.Encode(runsOn[0])
|
||||||
|
} else {
|
||||||
|
_ = node.Encode(runsOn)
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func nameWithMatrix(name string, m map[string]any, evaluator *ExpressionEvaluator) string {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(name, "${{") || !strings.Contains(name, "}}") {
|
||||||
|
return name + " " + matrixName(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return evaluator.Interpolate(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matrixName(m map[string]any) string {
|
||||||
|
ks := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
ks = append(ks, k)
|
||||||
|
}
|
||||||
|
sort.Strings(ks)
|
||||||
|
vs := make([]string, 0, len(m))
|
||||||
|
for _, v := range ks {
|
||||||
|
vs = append(vs, fmt.Sprint(m[v]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("(%s)", strings.Join(vs, ", "))
|
||||||
|
}
|
||||||
87
modules/actions/jobparser/jobparser_test.go
Normal file
87
modules/actions/jobparser/jobparser_test.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package jobparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.yaml.in/yaml/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
options []ParseOption
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "multiple_jobs",
|
||||||
|
options: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple_matrix",
|
||||||
|
options: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has_needs",
|
||||||
|
options: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has_with",
|
||||||
|
options: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has_secrets",
|
||||||
|
options: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty_step",
|
||||||
|
options: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "job_name_with_matrix",
|
||||||
|
options: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefixed_newline",
|
||||||
|
options: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
content := ReadTestdata(t, tt.name+".in.yaml")
|
||||||
|
want := ReadTestdata(t, tt.name+".out.yaml")
|
||||||
|
got, err := Parse(content, tt.options...)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
builder := &strings.Builder{}
|
||||||
|
for _, v := range got {
|
||||||
|
if builder.Len() > 0 {
|
||||||
|
builder.WriteString("---\n")
|
||||||
|
}
|
||||||
|
encoder := yaml.NewEncoder(builder)
|
||||||
|
encoder.SetIndent(2)
|
||||||
|
require.NoError(t, encoder.Encode(v))
|
||||||
|
id, job := v.Job()
|
||||||
|
assert.NotEmpty(t, id)
|
||||||
|
assert.NotNil(t, job)
|
||||||
|
}
|
||||||
|
assert.Equal(t, string(want), builder.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
497
modules/actions/jobparser/model.go
Normal file
497
modules/actions/jobparser/model.go
Normal file
@@ -0,0 +1,497 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package jobparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"go.yaml.in/yaml/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SingleWorkflow is a workflow with single job and single matrix
|
||||||
|
type SingleWorkflow struct {
|
||||||
|
Name string `yaml:"name,omitempty"`
|
||||||
|
RawOn yaml.Node `yaml:"on,omitempty"`
|
||||||
|
Env map[string]string `yaml:"env,omitempty"`
|
||||||
|
RawJobs yaml.Node `yaml:"jobs,omitempty"`
|
||||||
|
Defaults Defaults `yaml:"defaults,omitempty"`
|
||||||
|
RawPermissions yaml.Node `yaml:"permissions,omitempty"`
|
||||||
|
RunName string `yaml:"run-name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *SingleWorkflow) Job() (string, *Job) {
|
||||||
|
ids, jobs, _ := w.jobs()
|
||||||
|
if len(ids) >= 1 {
|
||||||
|
return ids[0], jobs[0]
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *SingleWorkflow) jobs() ([]string, []*Job, error) {
|
||||||
|
ids, jobs, err := parseMappingNode[*Job](&w.RawJobs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, job := range jobs {
|
||||||
|
steps := make([]*Step, 0, len(job.Steps))
|
||||||
|
for _, s := range job.Steps {
|
||||||
|
if s != nil {
|
||||||
|
steps = append(steps, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
job.Steps = steps
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, jobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *SingleWorkflow) SetJob(id string, job *Job) error {
|
||||||
|
m := map[string]*Job{
|
||||||
|
id: job,
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
encoder := yaml.NewEncoder(&buf)
|
||||||
|
encoder.SetIndent(2)
|
||||||
|
if err := encoder.Encode(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
encoder.Close()
|
||||||
|
|
||||||
|
node := yaml.Node{}
|
||||||
|
if err := yaml.Unmarshal(buf.Bytes(), &node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(node.Content) != 1 || node.Content[0].Kind != yaml.MappingNode {
|
||||||
|
return fmt.Errorf("can not set job: %s", buf.String())
|
||||||
|
}
|
||||||
|
w.RawJobs = *node.Content[0]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *SingleWorkflow) Marshal() ([]byte, error) {
|
||||||
|
return yaml.Marshal(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Job struct {
|
||||||
|
Name string `yaml:"name,omitempty"`
|
||||||
|
RawNeeds yaml.Node `yaml:"needs,omitempty"`
|
||||||
|
RawRunsOn yaml.Node `yaml:"runs-on,omitempty"`
|
||||||
|
Env yaml.Node `yaml:"env,omitempty"`
|
||||||
|
If yaml.Node `yaml:"if,omitempty"`
|
||||||
|
Steps []*Step `yaml:"steps,omitempty"`
|
||||||
|
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||||
|
Services map[string]*ContainerSpec `yaml:"services,omitempty"`
|
||||||
|
Strategy Strategy `yaml:"strategy,omitempty"`
|
||||||
|
RawContainer yaml.Node `yaml:"container,omitempty"`
|
||||||
|
Defaults Defaults `yaml:"defaults,omitempty"`
|
||||||
|
Outputs map[string]string `yaml:"outputs,omitempty"`
|
||||||
|
Uses string `yaml:"uses,omitempty"`
|
||||||
|
With map[string]any `yaml:"with,omitempty"`
|
||||||
|
RawSecrets yaml.Node `yaml:"secrets,omitempty"`
|
||||||
|
RawConcurrency *model.RawConcurrency `yaml:"concurrency,omitempty"`
|
||||||
|
RawPermissions yaml.Node `yaml:"permissions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) Clone() *Job {
|
||||||
|
if j == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &Job{
|
||||||
|
Name: j.Name,
|
||||||
|
RawNeeds: j.RawNeeds,
|
||||||
|
RawRunsOn: j.RawRunsOn,
|
||||||
|
Env: j.Env,
|
||||||
|
If: j.If,
|
||||||
|
Steps: j.Steps,
|
||||||
|
TimeoutMinutes: j.TimeoutMinutes,
|
||||||
|
Services: j.Services,
|
||||||
|
Strategy: j.Strategy,
|
||||||
|
RawContainer: j.RawContainer,
|
||||||
|
Defaults: j.Defaults,
|
||||||
|
Outputs: j.Outputs,
|
||||||
|
Uses: j.Uses,
|
||||||
|
With: j.With,
|
||||||
|
RawSecrets: j.RawSecrets,
|
||||||
|
RawConcurrency: j.RawConcurrency,
|
||||||
|
RawPermissions: j.RawPermissions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) Needs() []string {
|
||||||
|
return (&model.Job{RawNeeds: j.RawNeeds}).Needs()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) EraseNeeds() *Job {
|
||||||
|
j.RawNeeds = yaml.Node{}
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) RunsOn() []string {
|
||||||
|
return (&model.Job{RawRunsOn: j.RawRunsOn}).RunsOn()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Step struct {
|
||||||
|
ID string `yaml:"id,omitempty"`
|
||||||
|
If yaml.Node `yaml:"if,omitempty"`
|
||||||
|
Name string `yaml:"name,omitempty"`
|
||||||
|
Uses string `yaml:"uses,omitempty"`
|
||||||
|
Run string `yaml:"run,omitempty"`
|
||||||
|
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
||||||
|
Shell string `yaml:"shell,omitempty"`
|
||||||
|
Env yaml.Node `yaml:"env,omitempty"`
|
||||||
|
With map[string]string `yaml:"with,omitempty"`
|
||||||
|
ContinueOnError bool `yaml:"continue-on-error,omitempty"`
|
||||||
|
TimeoutMinutes string `yaml:"timeout-minutes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// String gets the name of step
|
||||||
|
func (s *Step) String() string {
|
||||||
|
if s == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return (&model.Step{
|
||||||
|
ID: s.ID,
|
||||||
|
Name: s.Name,
|
||||||
|
Uses: s.Uses,
|
||||||
|
Run: s.Run,
|
||||||
|
}).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerSpec struct {
|
||||||
|
Image string `yaml:"image,omitempty"`
|
||||||
|
Env map[string]string `yaml:"env,omitempty"`
|
||||||
|
Ports []string `yaml:"ports,omitempty"`
|
||||||
|
Volumes []string `yaml:"volumes,omitempty"`
|
||||||
|
Options string `yaml:"options,omitempty"`
|
||||||
|
Credentials map[string]string `yaml:"credentials,omitempty"`
|
||||||
|
Cmd []string `yaml:"cmd,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Strategy struct {
|
||||||
|
FailFastString string `yaml:"fail-fast,omitempty"`
|
||||||
|
MaxParallelString string `yaml:"max-parallel,omitempty"`
|
||||||
|
RawMatrix yaml.Node `yaml:"matrix,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Defaults struct {
|
||||||
|
Run RunDefaults `yaml:"run,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunDefaults struct {
|
||||||
|
Shell string `yaml:"shell,omitempty"`
|
||||||
|
WorkingDirectory string `yaml:"working-directory,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkflowDispatchInput struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Required bool `yaml:"required"`
|
||||||
|
Default string `yaml:"default"`
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Options []string `yaml:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Name string
|
||||||
|
acts map[string][]string
|
||||||
|
schedules []map[string]string
|
||||||
|
inputs []WorkflowDispatchInput
|
||||||
|
}
|
||||||
|
|
||||||
|
func (evt *Event) IsSchedule() bool {
|
||||||
|
return evt.schedules != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (evt *Event) Acts() map[string][]string {
|
||||||
|
return evt.acts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (evt *Event) Schedules() []map[string]string {
|
||||||
|
return evt.schedules
|
||||||
|
}
|
||||||
|
|
||||||
|
func (evt *Event) Inputs() []WorkflowDispatchInput {
|
||||||
|
return evt.inputs
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadWorkflowRawConcurrency(content []byte) (*model.RawConcurrency, error) {
|
||||||
|
w := new(model.Workflow)
|
||||||
|
err := yaml.NewDecoder(bytes.NewReader(content)).Decode(w)
|
||||||
|
return w.RawConcurrency, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func EvaluateConcurrency(rc *model.RawConcurrency, jobID string, job *Job, gitCtx map[string]any, results map[string]*JobResult, vars map[string]string, inputs map[string]any) (string, bool, error) {
|
||||||
|
actJob := &model.Job{}
|
||||||
|
if job != nil {
|
||||||
|
actJob.Strategy = &model.Strategy{
|
||||||
|
FailFastString: job.Strategy.FailFastString,
|
||||||
|
MaxParallelString: job.Strategy.MaxParallelString,
|
||||||
|
RawMatrix: job.Strategy.RawMatrix,
|
||||||
|
}
|
||||||
|
actJob.Strategy.FailFast = actJob.Strategy.GetFailFast()
|
||||||
|
actJob.Strategy.MaxParallel = actJob.Strategy.GetMaxParallel()
|
||||||
|
}
|
||||||
|
|
||||||
|
matrix := make(map[string]any)
|
||||||
|
matrixes, err := actJob.GetMatrixes()
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
if len(matrixes) > 0 {
|
||||||
|
matrix = matrixes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluator := NewExpressionEvaluator(NewInterpeter(jobID, actJob, matrix, toGitContext(gitCtx), results, vars, inputs))
|
||||||
|
var node yaml.Node
|
||||||
|
if err := node.Encode(rc); err != nil {
|
||||||
|
return "", false, fmt.Errorf("failed to encode concurrency: %w", err)
|
||||||
|
}
|
||||||
|
if err := evaluator.EvaluateYamlNode(&node); err != nil {
|
||||||
|
return "", false, fmt.Errorf("failed to evaluate concurrency: %w", err)
|
||||||
|
}
|
||||||
|
var evaluated model.RawConcurrency
|
||||||
|
if err := node.Decode(&evaluated); err != nil {
|
||||||
|
return "", false, fmt.Errorf("failed to unmarshal evaluated concurrency: %w", err)
|
||||||
|
}
|
||||||
|
if evaluated.RawExpression != "" {
|
||||||
|
return evaluated.RawExpression, false, nil
|
||||||
|
}
|
||||||
|
return evaluated.Group, evaluated.CancelInProgress == "true", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toGitContext(input map[string]any) *model.GithubContext {
|
||||||
|
gitContext := &model.GithubContext{
|
||||||
|
EventPath: asString(input["event_path"]),
|
||||||
|
Workflow: asString(input["workflow"]),
|
||||||
|
RunID: asString(input["run_id"]),
|
||||||
|
RunNumber: asString(input["run_number"]),
|
||||||
|
Actor: asString(input["actor"]),
|
||||||
|
Repository: asString(input["repository"]),
|
||||||
|
EventName: asString(input["event_name"]),
|
||||||
|
Sha: asString(input["sha"]),
|
||||||
|
Ref: asString(input["ref"]),
|
||||||
|
RefName: asString(input["ref_name"]),
|
||||||
|
RefType: asString(input["ref_type"]),
|
||||||
|
HeadRef: asString(input["head_ref"]),
|
||||||
|
BaseRef: asString(input["base_ref"]),
|
||||||
|
Token: asString(input["token"]),
|
||||||
|
Workspace: asString(input["workspace"]),
|
||||||
|
Action: asString(input["action"]),
|
||||||
|
ActionPath: asString(input["action_path"]),
|
||||||
|
ActionRef: asString(input["action_ref"]),
|
||||||
|
ActionRepository: asString(input["action_repository"]),
|
||||||
|
Job: asString(input["job"]),
|
||||||
|
RepositoryOwner: asString(input["repository_owner"]),
|
||||||
|
RetentionDays: asString(input["retention_days"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
event, ok := input["event"].(map[string]any)
|
||||||
|
if ok {
|
||||||
|
gitContext.Event = event
|
||||||
|
}
|
||||||
|
|
||||||
|
return gitContext
|
||||||
|
}
|
||||||
|
|
||||||
|
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 []any
|
||||||
|
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:
|
||||||
|
events, triggers, err := parseMappingNode[yaml.Node](rawOn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := make([]*Event, 0, len(events))
|
||||||
|
for i, k := range events {
|
||||||
|
v := triggers[i]
|
||||||
|
switch v.Kind {
|
||||||
|
case yaml.ScalarNode:
|
||||||
|
res = append(res, &Event{
|
||||||
|
Name: k,
|
||||||
|
})
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
var t []any
|
||||||
|
err := v.Decode(&t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
schedules := make([]map[string]string, len(t))
|
||||||
|
if k == "schedule" {
|
||||||
|
for i, tt := range t {
|
||||||
|
vv, ok := tt.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown on type(schedule): %#v", v)
|
||||||
|
}
|
||||||
|
schedules[i] = make(map[string]string, len(vv))
|
||||||
|
for k, vvv := range vv {
|
||||||
|
var ok bool
|
||||||
|
if schedules[i][k], ok = vvv.(string); !ok {
|
||||||
|
return nil, fmt.Errorf("unknown on type(schedule): %#v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(schedules) == 0 {
|
||||||
|
schedules = nil
|
||||||
|
}
|
||||||
|
res = append(res, &Event{
|
||||||
|
Name: k,
|
||||||
|
schedules: schedules,
|
||||||
|
})
|
||||||
|
case yaml.MappingNode:
|
||||||
|
acts := make(map[string][]string, len(v.Content)/2)
|
||||||
|
var inputs []WorkflowDispatchInput
|
||||||
|
expectedKey := true
|
||||||
|
var act string
|
||||||
|
for _, content := range v.Content {
|
||||||
|
if expectedKey {
|
||||||
|
if content.Kind != yaml.ScalarNode {
|
||||||
|
return nil, fmt.Errorf("key type not string: %#v", content)
|
||||||
|
}
|
||||||
|
act = ""
|
||||||
|
err := content.Decode(&act)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch content.Kind {
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
var t []string
|
||||||
|
err := content.Decode(&t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
acts[act] = t
|
||||||
|
case yaml.ScalarNode:
|
||||||
|
var t string
|
||||||
|
err := content.Decode(&t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
acts[act] = []string{t}
|
||||||
|
case yaml.MappingNode:
|
||||||
|
if k != "workflow_dispatch" || act != "inputs" {
|
||||||
|
return nil, fmt.Errorf("map should only for workflow_dispatch but %s: %#v", act, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
var key string
|
||||||
|
for i, vv := range content.Content {
|
||||||
|
if i%2 == 0 {
|
||||||
|
if vv.Kind != yaml.ScalarNode {
|
||||||
|
return nil, fmt.Errorf("key type not string: %#v", vv)
|
||||||
|
}
|
||||||
|
key = ""
|
||||||
|
if err := vv.Decode(&key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if vv.Kind != yaml.MappingNode {
|
||||||
|
return nil, fmt.Errorf("key type not map(%s): %#v", key, vv)
|
||||||
|
}
|
||||||
|
|
||||||
|
input := WorkflowDispatchInput{}
|
||||||
|
if err := vv.Decode(&input); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
input.Name = key
|
||||||
|
inputs = append(inputs, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown on type: %#v", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expectedKey = !expectedKey
|
||||||
|
}
|
||||||
|
if len(inputs) == 0 {
|
||||||
|
inputs = nil
|
||||||
|
}
|
||||||
|
if len(acts) == 0 {
|
||||||
|
acts = nil
|
||||||
|
}
|
||||||
|
res = append(res, &Event{
|
||||||
|
Name: k,
|
||||||
|
acts: acts,
|
||||||
|
inputs: inputs,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown on type: %v", v.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown on type: %v", rawOn.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMappingNode parse a mapping node and preserve order.
|
||||||
|
func parseMappingNode[T any](node *yaml.Node) ([]string, []T, error) {
|
||||||
|
if node.Kind != yaml.MappingNode {
|
||||||
|
return nil, nil, errors.New("input node is not a mapping node")
|
||||||
|
}
|
||||||
|
|
||||||
|
var scalars []string
|
||||||
|
var datas []T
|
||||||
|
expectKey := true
|
||||||
|
for _, item := range node.Content {
|
||||||
|
if expectKey {
|
||||||
|
if item.Kind != yaml.ScalarNode {
|
||||||
|
return nil, nil, fmt.Errorf("not a valid scalar node: %v", item.Value)
|
||||||
|
}
|
||||||
|
scalars = append(scalars, item.Value)
|
||||||
|
expectKey = false
|
||||||
|
} else {
|
||||||
|
var val T
|
||||||
|
if err := item.Decode(&val); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
datas = append(datas, val)
|
||||||
|
expectKey = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(scalars) != len(datas) {
|
||||||
|
return nil, nil, fmt.Errorf("invalid definition of on: %v", node.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return scalars, datas, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func asString(v any) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
} else if s, ok := v.(string); ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
373
modules/actions/jobparser/model_test.go
Normal file
373
modules/actions/jobparser/model_test.go
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package jobparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.yaml.in/yaml/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 push:\n branches: main",
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "push",
|
||||||
|
acts: map[string][]string{
|
||||||
|
"branches": {
|
||||||
|
"main",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n schedule:\n - cron: '20 6 * * *'",
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "schedule",
|
||||||
|
schedules: []map[string]string{
|
||||||
|
{
|
||||||
|
"cron": "20 6 * * *",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
logLevel:
|
||||||
|
description: 'Log level'
|
||||||
|
required: true
|
||||||
|
default: 'warning'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- info
|
||||||
|
- warning
|
||||||
|
- debug
|
||||||
|
tags:
|
||||||
|
description: 'Test scenario tags'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
environment:
|
||||||
|
description: 'Environment to run tests against'
|
||||||
|
type: environment
|
||||||
|
required: true
|
||||||
|
push:
|
||||||
|
`,
|
||||||
|
result: []*Event{
|
||||||
|
{
|
||||||
|
Name: "workflow_dispatch",
|
||||||
|
inputs: []WorkflowDispatchInput{
|
||||||
|
{
|
||||||
|
Name: "logLevel",
|
||||||
|
Description: "Log level",
|
||||||
|
Required: true,
|
||||||
|
Default: "warning",
|
||||||
|
Type: "choice",
|
||||||
|
Options: []string{"info", "warning", "debug"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tags",
|
||||||
|
Description: "Test scenario tags",
|
||||||
|
Required: false,
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "environment",
|
||||||
|
Description: "Environment to run tests against",
|
||||||
|
Type: "environment",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "push",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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.Equal(t, kase.result, events, events)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSingleWorkflow_SetJob(t *testing.T) {
|
||||||
|
t.Run("erase needs", func(t *testing.T) {
|
||||||
|
content := ReadTestdata(t, "erase_needs.in.yaml")
|
||||||
|
want := ReadTestdata(t, "erase_needs.out.yaml")
|
||||||
|
swf, err := Parse(content)
|
||||||
|
require.NoError(t, err)
|
||||||
|
builder := &strings.Builder{}
|
||||||
|
for _, v := range swf {
|
||||||
|
id, job := v.Job()
|
||||||
|
require.NoError(t, v.SetJob(id, job.EraseNeeds()))
|
||||||
|
|
||||||
|
if builder.Len() > 0 {
|
||||||
|
builder.WriteString("---\n")
|
||||||
|
}
|
||||||
|
encoder := yaml.NewEncoder(builder)
|
||||||
|
encoder.SetIndent(2)
|
||||||
|
require.NoError(t, encoder.Encode(v))
|
||||||
|
}
|
||||||
|
assert.Equal(t, string(want), builder.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMappingNode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
scalars []string
|
||||||
|
datas []any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "on:\n push:\n branches:\n - master",
|
||||||
|
scalars: []string{"push"},
|
||||||
|
datas: []any{
|
||||||
|
map[string]any{
|
||||||
|
"branches": []any{"master"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n branch_protection_rule:\n types: [created, deleted]",
|
||||||
|
scalars: []string{"branch_protection_rule"},
|
||||||
|
datas: []any{
|
||||||
|
map[string]any{
|
||||||
|
"types": []any{"created", "deleted"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n project:\n types: [created, deleted]\n milestone:\n types: [opened, deleted]",
|
||||||
|
scalars: []string{"project", "milestone"},
|
||||||
|
datas: []any{
|
||||||
|
map[string]any{
|
||||||
|
"types": []any{"created", "deleted"},
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"types": []any{"opened", "deleted"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n pull_request:\n types:\n - opened\n branches:\n - 'releases/**'",
|
||||||
|
scalars: []string{"pull_request"},
|
||||||
|
datas: []any{
|
||||||
|
map[string]any{
|
||||||
|
"types": []any{"opened"},
|
||||||
|
"branches": []any{"releases/**"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n push:\n branches:\n - main\n pull_request:\n types:\n - opened\n branches:\n - '**'",
|
||||||
|
scalars: []string{"push", "pull_request"},
|
||||||
|
datas: []any{
|
||||||
|
map[string]any{
|
||||||
|
"branches": []any{"main"},
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"types": []any{"opened"},
|
||||||
|
"branches": []any{"**"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "on:\n schedule:\n - cron: '20 6 * * *'",
|
||||||
|
scalars: []string{"schedule"},
|
||||||
|
datas: []any{
|
||||||
|
[]any{map[string]any{
|
||||||
|
"cron": "20 6 * * *",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.input, func(t *testing.T) {
|
||||||
|
workflow, err := model.ReadWorkflow(strings.NewReader(test.input))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
scalars, datas, err := parseMappingNode[any](&workflow.RawOn)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, test.scalars, scalars, scalars)
|
||||||
|
assert.Equal(t, test.datas, datas, datas)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
8
modules/actions/jobparser/testdata/empty_step.in.yaml
vendored
Normal file
8
modules/actions/jobparser/testdata/empty_step.in.yaml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: echo job-1
|
||||||
|
-
|
||||||
7
modules/actions/jobparser/testdata/empty_step.out.yaml
vendored
Normal file
7
modules/actions/jobparser/testdata/empty_step.out.yaml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: echo job-1
|
||||||
16
modules/actions/jobparser/testdata/erase_needs.in.yaml
vendored
Normal file
16
modules/actions/jobparser/testdata/erase_needs.in.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
job2:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
needs: job1
|
||||||
|
job3:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
needs: [job1, job2]
|
||||||
23
modules/actions/jobparser/testdata/erase_needs.out.yaml
vendored
Normal file
23
modules/actions/jobparser/testdata/erase_needs.out.yaml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job2:
|
||||||
|
name: job2
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job3:
|
||||||
|
name: job3
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
16
modules/actions/jobparser/testdata/has_needs.in.yaml
vendored
Normal file
16
modules/actions/jobparser/testdata/has_needs.in.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
job2:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
needs: job1
|
||||||
|
job3:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
needs: [job1, job2]
|
||||||
25
modules/actions/jobparser/testdata/has_needs.out.yaml
vendored
Normal file
25
modules/actions/jobparser/testdata/has_needs.out.yaml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job2:
|
||||||
|
name: job2
|
||||||
|
needs: job1
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job3:
|
||||||
|
name: job3
|
||||||
|
needs: [job1, job2]
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a
|
||||||
14
modules/actions/jobparser/testdata/has_secrets.in.yaml
vendored
Normal file
14
modules/actions/jobparser/testdata/has_secrets.in.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1
|
||||||
|
runs-on: linux
|
||||||
|
uses: .gitea/workflows/build.yml
|
||||||
|
secrets:
|
||||||
|
secret: hideme
|
||||||
|
|
||||||
|
job2:
|
||||||
|
name: job2
|
||||||
|
runs-on: linux
|
||||||
|
uses: .gitea/workflows/build.yml
|
||||||
|
secrets: inherit
|
||||||
16
modules/actions/jobparser/testdata/has_secrets.out.yaml
vendored
Normal file
16
modules/actions/jobparser/testdata/has_secrets.out.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1
|
||||||
|
runs-on: linux
|
||||||
|
uses: .gitea/workflows/build.yml
|
||||||
|
secrets:
|
||||||
|
secret: hideme
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job2:
|
||||||
|
name: job2
|
||||||
|
runs-on: linux
|
||||||
|
uses: .gitea/workflows/build.yml
|
||||||
|
secrets: inherit
|
||||||
15
modules/actions/jobparser/testdata/has_with.in.yaml
vendored
Normal file
15
modules/actions/jobparser/testdata/has_with.in.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1
|
||||||
|
runs-on: linux
|
||||||
|
uses: .gitea/workflows/build.yml
|
||||||
|
with:
|
||||||
|
package: service
|
||||||
|
|
||||||
|
job2:
|
||||||
|
name: job2
|
||||||
|
runs-on: linux
|
||||||
|
uses: .gitea/workflows/build.yml
|
||||||
|
with:
|
||||||
|
package: module
|
||||||
17
modules/actions/jobparser/testdata/has_with.out.yaml
vendored
Normal file
17
modules/actions/jobparser/testdata/has_with.out.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1
|
||||||
|
runs-on: linux
|
||||||
|
uses: .gitea/workflows/build.yml
|
||||||
|
with:
|
||||||
|
package: service
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job2:
|
||||||
|
name: job2
|
||||||
|
runs-on: linux
|
||||||
|
uses: .gitea/workflows/build.yml
|
||||||
|
with:
|
||||||
|
package: module
|
||||||
14
modules/actions/jobparser/testdata/job_name_with_matrix.in.yaml
vendored
Normal file
14
modules/actions/jobparser/testdata/job_name_with_matrix.in.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-22.04, ubuntu-20.04]
|
||||||
|
version: [1.17, 1.18, 1.19]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
name: test_version_${{ matrix.version }}_on_${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
101
modules/actions/jobparser/testdata/job_name_with_matrix.out.yaml
vendored
Normal file
101
modules/actions/jobparser/testdata/job_name_with_matrix.out.yaml
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: test_version_1.17_on_ubuntu-20.04
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-20.04
|
||||||
|
version:
|
||||||
|
- 1.17
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: test_version_1.18_on_ubuntu-20.04
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-20.04
|
||||||
|
version:
|
||||||
|
- 1.18
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: test_version_1.19_on_ubuntu-20.04
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-20.04
|
||||||
|
version:
|
||||||
|
- 1.19
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: test_version_1.17_on_ubuntu-22.04
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-22.04
|
||||||
|
version:
|
||||||
|
- 1.17
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: test_version_1.18_on_ubuntu-22.04
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-22.04
|
||||||
|
version:
|
||||||
|
- 1.18
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: test_version_1.19_on_ubuntu-22.04
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-22.04
|
||||||
|
version:
|
||||||
|
- 1.19
|
||||||
22
modules/actions/jobparser/testdata/multiple_jobs.in.yaml
vendored
Normal file
22
modules/actions/jobparser/testdata/multiple_jobs.in.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
zzz:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: echo zzz
|
||||||
|
job1:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
||||||
|
job2:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
||||||
|
job3:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
||||||
|
aaa:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
||||||
39
modules/actions/jobparser/testdata/multiple_jobs.out.yaml
vendored
Normal file
39
modules/actions/jobparser/testdata/multiple_jobs.out.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
zzz:
|
||||||
|
name: zzz
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: echo zzz
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job2:
|
||||||
|
name: job2
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job3:
|
||||||
|
name: job3
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
aaa:
|
||||||
|
name: aaa
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- run: uname -a && go version
|
||||||
13
modules/actions/jobparser/testdata/multiple_matrix.in.yaml
vendored
Normal file
13
modules/actions/jobparser/testdata/multiple_matrix.in.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-22.04, ubuntu-20.04]
|
||||||
|
version: [1.17, 1.18, 1.19]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
101
modules/actions/jobparser/testdata/multiple_matrix.out.yaml
vendored
Normal file
101
modules/actions/jobparser/testdata/multiple_matrix.out.yaml
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1 (ubuntu-20.04, 1.17)
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-20.04
|
||||||
|
version:
|
||||||
|
- 1.17
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1 (ubuntu-20.04, 1.18)
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-20.04
|
||||||
|
version:
|
||||||
|
- 1.18
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1 (ubuntu-20.04, 1.19)
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-20.04
|
||||||
|
version:
|
||||||
|
- 1.19
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1 (ubuntu-22.04, 1.17)
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-22.04
|
||||||
|
version:
|
||||||
|
- 1.17
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1 (ubuntu-22.04, 1.18)
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-22.04
|
||||||
|
version:
|
||||||
|
- 1.18
|
||||||
|
---
|
||||||
|
name: test
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
name: job1 (ubuntu-22.04, 1.19)
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.version }}
|
||||||
|
- run: uname -a && go version
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-22.04
|
||||||
|
version:
|
||||||
|
- 1.19
|
||||||
14
modules/actions/jobparser/testdata/prefixed_newline.in.yaml
vendored
Normal file
14
modules/actions/jobparser/testdata/prefixed_newline.in.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
name: Step with leading new line
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: "\nExtract tag for variant"
|
||||||
|
id: extract_tag
|
||||||
|
run: |
|
||||||
|
|
||||||
|
echo Test
|
||||||
15
modules/actions/jobparser/testdata/prefixed_newline.out.yaml
vendored
Normal file
15
modules/actions/jobparser/testdata/prefixed_newline.out.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
name: Step with leading new line
|
||||||
|
"on":
|
||||||
|
push:
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- id: extract_tag
|
||||||
|
name: |2-
|
||||||
|
|
||||||
|
Extract tag for variant
|
||||||
|
run: |2
|
||||||
|
|
||||||
|
echo Test
|
||||||
21
modules/actions/jobparser/testdata_test.go
Normal file
21
modules/actions/jobparser/testdata_test.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package jobparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed testdata
|
||||||
|
var testdata embed.FS
|
||||||
|
|
||||||
|
func ReadTestdata(t *testing.T, name string) []byte {
|
||||||
|
content, err := testdata.ReadFile(filepath.Join("testdata", name))
|
||||||
|
require.NoError(t, err)
|
||||||
|
return content
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/actions/jobparser"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/glob"
|
"code.gitea.io/gitea/modules/glob"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
@@ -16,10 +17,9 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/jobparser"
|
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
"github.com/nektos/act/pkg/workflowpattern"
|
"github.com/nektos/act/pkg/workflowpattern"
|
||||||
"gopkg.in/yaml.v3"
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DetectedWorkflow struct {
|
type DetectedWorkflow struct {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import (
|
|||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
|
||||||
act_model "github.com/nektos/act/pkg/model"
|
act_model "github.com/nektos/act/pkg/model"
|
||||||
"gopkg.in/yaml.v3"
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import (
|
|||||||
notify_service "code.gitea.io/gitea/services/notify"
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
"gopkg.in/yaml.v3"
|
"go.yaml.in/yaml/v4"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -446,7 +446,7 @@ func Rerun(ctx *context_module.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = actions_service.EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars)
|
err = actions_service.EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("EvaluateRunConcurrencyFillModel", err)
|
ctx.ServerError("EvaluateRunConcurrencyFillModel", err)
|
||||||
return
|
return
|
||||||
@@ -526,7 +526,7 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou
|
|||||||
}
|
}
|
||||||
|
|
||||||
if job.RawConcurrency != "" && !shouldBlock {
|
if job.RawConcurrency != "" && !shouldBlock {
|
||||||
err = actions_service.EvaluateJobConcurrencyFillModel(ctx, job.Run, job, vars)
|
err = actions_service.EvaluateJobConcurrencyFillModel(ctx, job.Run, job, vars, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("evaluate job concurrency: %w", err)
|
return fmt.Errorf("evaluate job concurrency: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,12 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
actions_module "code.gitea.io/gitea/modules/actions"
|
actions_module "code.gitea.io/gitea/modules/actions"
|
||||||
|
"code.gitea.io/gitea/modules/actions/jobparser"
|
||||||
"code.gitea.io/gitea/modules/commitstatus"
|
"code.gitea.io/gitea/modules/commitstatus"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
|
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/jobparser"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateCommitStatusForRunJobs creates a commit status for the given job if it has a supported event and related commit.
|
// CreateCommitStatusForRunJobs creates a commit status for the given job if it has a supported event and related commit.
|
||||||
|
|||||||
@@ -8,28 +8,31 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/modules/actions/jobparser"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/jobparser"
|
|
||||||
act_model "github.com/nektos/act/pkg/model"
|
act_model "github.com/nektos/act/pkg/model"
|
||||||
"gopkg.in/yaml.v3"
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EvaluateRunConcurrencyFillModel evaluates the expressions in a run-level (workflow) concurrency,
|
// EvaluateRunConcurrencyFillModel evaluates the expressions in a run-level (workflow) concurrency,
|
||||||
// and fills the run's model fields with `concurrency.group` and `concurrency.cancel-in-progress`.
|
// and fills the run's model fields with `concurrency.group` and `concurrency.cancel-in-progress`.
|
||||||
// Workflow-level concurrency doesn't depend on the job outputs, so it can always be evaluated if there is no syntax error.
|
// Workflow-level concurrency doesn't depend on the job outputs, so it can always be evaluated if there is no syntax error.
|
||||||
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency
|
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency
|
||||||
func EvaluateRunConcurrencyFillModel(ctx context.Context, run *actions_model.ActionRun, wfRawConcurrency *act_model.RawConcurrency, vars map[string]string) error {
|
func EvaluateRunConcurrencyFillModel(ctx context.Context, run *actions_model.ActionRun, wfRawConcurrency *act_model.RawConcurrency, vars map[string]string, inputs map[string]any) error {
|
||||||
if err := run.LoadAttributes(ctx); err != nil {
|
if err := run.LoadAttributes(ctx); err != nil {
|
||||||
return fmt.Errorf("run LoadAttributes: %w", err)
|
return fmt.Errorf("run LoadAttributes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actionsRunCtx := GenerateGiteaContext(run, nil)
|
actionsRunCtx := GenerateGiteaContext(run, nil)
|
||||||
jobResults := map[string]*jobparser.JobResult{"": {}}
|
jobResults := map[string]*jobparser.JobResult{"": {}}
|
||||||
inputs, err := getInputsFromRun(run)
|
if inputs == nil {
|
||||||
if err != nil {
|
var err error
|
||||||
return fmt.Errorf("get inputs: %w", err)
|
inputs, err = getInputsFromRun(run)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get inputs: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rawConcurrency, err := yaml.Marshal(wfRawConcurrency)
|
rawConcurrency, err := yaml.Marshal(wfRawConcurrency)
|
||||||
@@ -68,7 +71,7 @@ func findJobNeedsAndFillJobResults(ctx context.Context, job *actions_model.Actio
|
|||||||
// Job-level concurrency may depend on other job's outputs (via `needs`): `concurrency.group: my-group-${{ needs.job1.outputs.out1 }}`
|
// Job-level concurrency may depend on other job's outputs (via `needs`): `concurrency.group: my-group-${{ needs.job1.outputs.out1 }}`
|
||||||
// If the needed jobs haven't been executed yet, this evaluation will also fail.
|
// If the needed jobs haven't been executed yet, this evaluation will also fail.
|
||||||
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idconcurrency
|
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idconcurrency
|
||||||
func EvaluateJobConcurrencyFillModel(ctx context.Context, run *actions_model.ActionRun, actionRunJob *actions_model.ActionRunJob, vars map[string]string) error {
|
func EvaluateJobConcurrencyFillModel(ctx context.Context, run *actions_model.ActionRun, actionRunJob *actions_model.ActionRunJob, vars map[string]string, inputs map[string]any) error {
|
||||||
if err := actionRunJob.LoadAttributes(ctx); err != nil {
|
if err := actionRunJob.LoadAttributes(ctx); err != nil {
|
||||||
return fmt.Errorf("job LoadAttributes: %w", err)
|
return fmt.Errorf("job LoadAttributes: %w", err)
|
||||||
}
|
}
|
||||||
@@ -85,9 +88,12 @@ func EvaluateJobConcurrencyFillModel(ctx context.Context, run *actions_model.Act
|
|||||||
return fmt.Errorf("find job needs and fill job results: %w", err)
|
return fmt.Errorf("find job needs and fill job results: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
inputs, err := getInputsFromRun(run)
|
if inputs == nil {
|
||||||
if err != nil {
|
var err error
|
||||||
return fmt.Errorf("get inputs: %w", err)
|
inputs, err = getInputsFromRun(run)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get inputs: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
workflowJob, err := actionRunJob.ParseJob()
|
workflowJob, err := actionRunJob.ParseJob()
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ func updateConcurrencyEvaluationForJobWithNeeds(ctx context.Context, actionRunJo
|
|||||||
return nil // for testing purpose only, no repo, no evaluation
|
return nil // for testing purpose only, no repo, no evaluation
|
||||||
}
|
}
|
||||||
|
|
||||||
err := EvaluateJobConcurrencyFillModel(ctx, actionRunJob.Run, actionRunJob, vars)
|
err := EvaluateJobConcurrencyFillModel(ctx, actionRunJob.Run, actionRunJob, vars, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("evaluate job concurrency: %w", err)
|
return fmt.Errorf("evaluate job concurrency: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import (
|
|||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/actions/jobparser"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
notify_service "code.gitea.io/gitea/services/notify"
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/jobparser"
|
"go.yaml.in/yaml/v4"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrepareRunAndInsert prepares a run and inserts it into the database
|
// PrepareRunAndInsert prepares a run and inserts it into the database
|
||||||
@@ -35,7 +35,7 @@ func PrepareRunAndInsert(ctx context.Context, content []byte, run *actions_model
|
|||||||
}
|
}
|
||||||
|
|
||||||
if wfRawConcurrency != nil {
|
if wfRawConcurrency != nil {
|
||||||
err = EvaluateRunConcurrencyFillModel(ctx, run, wfRawConcurrency, vars)
|
err = EvaluateRunConcurrencyFillModel(ctx, run, wfRawConcurrency, vars, inputsWithDefaults)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("EvaluateRunConcurrencyFillModel: %w", err)
|
return fmt.Errorf("EvaluateRunConcurrencyFillModel: %w", err)
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ func PrepareRunAndInsert(ctx context.Context, content []byte, run *actions_model
|
|||||||
run.Title = jobs[0].RunName
|
run.Title = jobs[0].RunName
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = InsertRun(ctx, run, jobs, vars); err != nil {
|
if err = InsertRun(ctx, run, jobs, vars, inputsWithDefaults); err != nil {
|
||||||
return fmt.Errorf("InsertRun: %w", err)
|
return fmt.Errorf("InsertRun: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ func PrepareRunAndInsert(ctx context.Context, content []byte, run *actions_model
|
|||||||
|
|
||||||
// InsertRun inserts a run
|
// InsertRun inserts a run
|
||||||
// The title will be cut off at 255 characters if it's longer than 255 characters.
|
// The title will be cut off at 255 characters if it's longer than 255 characters.
|
||||||
func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobparser.SingleWorkflow, vars map[string]string) error {
|
func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobparser.SingleWorkflow, vars map[string]string, inputs map[string]any) error {
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID)
|
index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -137,7 +137,7 @@ func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobpar
|
|||||||
|
|
||||||
// do not evaluate job concurrency when it requires `needs`, the jobs with `needs` will be evaluated later by job emitter
|
// do not evaluate job concurrency when it requires `needs`, the jobs with `needs` will be evaluated later by job emitter
|
||||||
if len(needs) == 0 {
|
if len(needs) == 0 {
|
||||||
err = EvaluateJobConcurrencyFillModel(ctx, run, runJob, vars)
|
err = EvaluateJobConcurrencyFillModel(ctx, run, runJob, vars, inputs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("evaluate job concurrency: %w", err)
|
return fmt.Errorf("evaluate job concurrency: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/actions"
|
"code.gitea.io/gitea/modules/actions"
|
||||||
|
"code.gitea.io/gitea/modules/actions/jobparser"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/reqctx"
|
"code.gitea.io/gitea/modules/reqctx"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
@@ -21,9 +22,8 @@ import (
|
|||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/jobparser"
|
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
"gopkg.in/yaml.v3"
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnable bool) error {
|
func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnable bool) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user