mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-03-22 06:45:03 +01:00
feat: allow ctx overlay + case sensitive env ctx (#99)
* switch to fork of actionlint
This commit is contained in:
2
go.mod
2
go.mod
@@ -110,3 +110,5 @@ require (
|
|||||||
google.golang.org/grpc v1.66.3 // indirect
|
google.golang.org/grpc v1.66.3 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/rhysd/actionlint => github.com/actions-oss/act-cli-actionlint v0.0.0-20250517100532-8f847f29ba36
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -15,6 +15,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n
|
|||||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||||
|
github.com/actions-oss/act-cli-actionlint v0.0.0-20250517100532-8f847f29ba36 h1:QnIPcWM4eVfqRUB3B6sLOwEJrMrTa64qrVqzxF5A21U=
|
||||||
|
github.com/actions-oss/act-cli-actionlint v0.0.0-20250517100532-8f847f29ba36/go.mod h1:AE6I6vJEkNaIfWqC2GNE5spIJNhxf8NCtLEKU4NnUXg=
|
||||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||||
github.com/andreaskoch/go-fswatch v1.0.0 h1:la8nP/HiaFCxP2IM6NZNUCoxgLWuyNFgH0RligBbnJU=
|
github.com/andreaskoch/go-fswatch v1.0.0 h1:la8nP/HiaFCxP2IM6NZNUCoxgLWuyNFgH0RligBbnJU=
|
||||||
@@ -164,8 +166,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rhysd/actionlint v1.7.7 h1:0KgkoNTrYY7vmOCs9BW2AHxLvvpoY9nEUzgBHiPUr0k=
|
|
||||||
github.com/rhysd/actionlint v1.7.7/go.mod h1:AE6I6vJEkNaIfWqC2GNE5spIJNhxf8NCtLEKU4NnUXg=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package exprparser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding"
|
"encoding"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -25,8 +26,12 @@ type EvaluationEnvironment struct {
|
|||||||
Needs map[string]Needs
|
Needs map[string]Needs
|
||||||
Inputs map[string]interface{}
|
Inputs map[string]interface{}
|
||||||
HashFiles func([]reflect.Value) (interface{}, error)
|
HashFiles func([]reflect.Value) (interface{}, error)
|
||||||
|
EnvCS bool
|
||||||
|
CtxData map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CaseSensitiveDict map[string]string
|
||||||
|
|
||||||
type Needs struct {
|
type Needs struct {
|
||||||
Outputs map[string]string `json:"outputs"`
|
Outputs map[string]string `json:"outputs"`
|
||||||
Result string `json:"result"`
|
Result string `json:"result"`
|
||||||
@@ -151,10 +156,17 @@ func (impl *interperterImpl) evaluateNode(exprNode actionlint.ExprNode) (interfa
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (impl *interperterImpl) evaluateVariable(variableNode *actionlint.VariableNode) (interface{}, error) {
|
func (impl *interperterImpl) evaluateVariable(variableNode *actionlint.VariableNode) (interface{}, error) {
|
||||||
switch strings.ToLower(variableNode.Name) {
|
lowerName := strings.ToLower(variableNode.Name)
|
||||||
|
if result, err := impl.evaluateOverriddenVariable(lowerName); result != nil || err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
switch lowerName {
|
||||||
case "github":
|
case "github":
|
||||||
return impl.env.Github, nil
|
return impl.env.Github, nil
|
||||||
case "env":
|
case "env":
|
||||||
|
if impl.env.EnvCS {
|
||||||
|
return CaseSensitiveDict(impl.env.Env), nil
|
||||||
|
}
|
||||||
return impl.env.Env, nil
|
return impl.env.Env, nil
|
||||||
case "job":
|
case "job":
|
||||||
return impl.env.Job, nil
|
return impl.env.Job, nil
|
||||||
@@ -188,6 +200,33 @@ func (impl *interperterImpl) evaluateVariable(variableNode *actionlint.VariableN
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (impl *interperterImpl) evaluateOverriddenVariable(lowerName string) (interface{}, error) {
|
||||||
|
if cd, ok := impl.env.CtxData[lowerName]; ok {
|
||||||
|
if serverPayload, ok := cd.(map[string]interface{}); ok {
|
||||||
|
if lowerName == "github" {
|
||||||
|
var out map[string]interface{}
|
||||||
|
content, err := json.Marshal(impl.env.Github)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(content, &out)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for k, v := range serverPayload {
|
||||||
|
// skip empty values, because github.workspace was set by Gitea Actions to an empty string
|
||||||
|
if _, ok := out[k]; !ok || v != "" && v != nil {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cd, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (impl *interperterImpl) evaluateIndexAccess(indexAccessNode *actionlint.IndexAccessNode) (interface{}, error) {
|
func (impl *interperterImpl) evaluateIndexAccess(indexAccessNode *actionlint.IndexAccessNode) (interface{}, error) {
|
||||||
left, err := impl.evaluateNode(indexAccessNode.Operand)
|
left, err := impl.evaluateNode(indexAccessNode.Operand)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -280,6 +319,11 @@ func (impl *interperterImpl) getPropertyValue(left reflect.Value, property strin
|
|||||||
return i, nil
|
return i, nil
|
||||||
|
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
|
cd, ok := left.Interface().(CaseSensitiveDict)
|
||||||
|
if ok {
|
||||||
|
return cd[property], nil
|
||||||
|
}
|
||||||
|
|
||||||
iter := left.MapRange()
|
iter := left.MapRange()
|
||||||
|
|
||||||
for iter.Next() {
|
for iter.Next() {
|
||||||
|
|||||||
@@ -528,46 +528,58 @@ func TestOperatorsBooleanEvaluation(t *testing.T) {
|
|||||||
|
|
||||||
func TestContexts(t *testing.T) {
|
func TestContexts(t *testing.T) {
|
||||||
table := []struct {
|
table := []struct {
|
||||||
input string
|
input string
|
||||||
expected interface{}
|
expected interface{}
|
||||||
name string
|
name string
|
||||||
|
caseSensitiveEnv bool
|
||||||
|
ctxdata map[string]interface{}
|
||||||
}{
|
}{
|
||||||
{"github.action", "push", "github-context"},
|
{input: "github.action", expected: "push", name: "github-context"},
|
||||||
{"github.event.commits[0].message", nil, "github-context-noexist-prop"},
|
{input: "github.action", expected: "push", name: "github-context", ctxdata: map[string]interface{}{"github": map[string]interface{}{"ref": "refs/heads/test-data"}}},
|
||||||
{"fromjson('{\"commits\":[]}').commits[0].message", nil, "github-context-noexist-prop"},
|
{input: "github.ref", expected: "refs/heads/test-data", name: "github-context", ctxdata: map[string]interface{}{"github": map[string]interface{}{"ref": "refs/heads/test-data"}}},
|
||||||
{"github.event.pull_request.labels.*.name", nil, "github-context-noexist-prop"},
|
{input: "github.custom-field", expected: "custom-value", name: "github-context", ctxdata: map[string]interface{}{"github": map[string]interface{}{"custom-field": "custom-value"}}},
|
||||||
{"env.TEST", "value", "env-context"},
|
{input: "github.event.commits[0].message", expected: nil, name: "github-context-noexist-prop"},
|
||||||
{"job.status", "success", "job-context"},
|
{input: "fromjson('{\"commits\":[]}').commits[0].message", expected: nil, name: "github-context-noexist-prop"},
|
||||||
{"steps.step-id.outputs.name", "value", "steps-context"},
|
{input: "github.event.pull_request.labels.*.name", expected: nil, name: "github-context-noexist-prop"},
|
||||||
{"steps.step-id.conclusion", "success", "steps-context-conclusion"},
|
{input: "env.TEST", expected: "value", name: "env-context"},
|
||||||
{"steps.step-id.conclusion && true", true, "steps-context-conclusion"},
|
{input: "env.TEST", expected: "value", name: "env-context", caseSensitiveEnv: true},
|
||||||
{"steps.step-id2.conclusion", "skipped", "steps-context-conclusion"},
|
{input: "env.test", expected: "", name: "env-context", caseSensitiveEnv: true},
|
||||||
{"steps.step-id2.conclusion && true", true, "steps-context-conclusion"},
|
{input: "env['TEST']", expected: "value", name: "env-context", caseSensitiveEnv: true},
|
||||||
{"steps.step-id.outcome", "success", "steps-context-outcome"},
|
{input: "env['test']", expected: "", name: "env-context", caseSensitiveEnv: true},
|
||||||
{"steps.step-id['outcome']", "success", "steps-context-outcome"},
|
{input: "env.test", expected: "value", name: "env-context"},
|
||||||
{"steps.step-id.outcome == 'success'", true, "steps-context-outcome"},
|
{input: "job.status", expected: "success", name: "job-context"},
|
||||||
{"steps.step-id['outcome'] == 'success'", true, "steps-context-outcome"},
|
{input: "steps.step-id.outputs.name", expected: "value", name: "steps-context"},
|
||||||
{"steps.step-id.outcome && true", true, "steps-context-outcome"},
|
{input: "steps.step-id.conclusion", expected: "success", name: "steps-context-conclusion"},
|
||||||
{"steps['step-id']['outcome'] && true", true, "steps-context-outcome"},
|
{input: "steps.step-id.conclusion && true", expected: true, name: "steps-context-conclusion"},
|
||||||
{"steps.step-id2.outcome", "failure", "steps-context-outcome"},
|
{input: "steps.step-id2.conclusion", expected: "skipped", name: "steps-context-conclusion"},
|
||||||
{"steps.step-id2.outcome && true", true, "steps-context-outcome"},
|
{input: "steps.step-id2.conclusion && true", expected: true, name: "steps-context-conclusion"},
|
||||||
|
{input: "steps.step-id.outcome", expected: "success", name: "steps-context-outcome"},
|
||||||
|
{input: "steps.step-id['outcome']", expected: "success", name: "steps-context-outcome"},
|
||||||
|
{input: "steps.step-id.outcome == 'success'", expected: true, name: "steps-context-outcome"},
|
||||||
|
{input: "steps.step-id['outcome'] == 'success'", expected: true, name: "steps-context-outcome"},
|
||||||
|
{input: "steps.step-id.outcome && true", expected: true, name: "steps-context-outcome"},
|
||||||
|
{input: "steps['step-id']['outcome'] && true", expected: true, name: "steps-context-outcome"},
|
||||||
|
{input: "steps.step-id2.outcome", expected: "failure", name: "steps-context-outcome"},
|
||||||
|
{input: "steps.step-id2.outcome && true", expected: true, name: "steps-context-outcome"},
|
||||||
// Disabled, since the interpreter is still too broken
|
// Disabled, since the interpreter is still too broken
|
||||||
// {"contains(steps.*.outcome, 'success')", true, "steps-context-array-outcome"},
|
// {"contains(steps.*.outcome, 'success')", true, "steps-context-array-outcome"},
|
||||||
// {"contains(steps.*.outcome, 'failure')", true, "steps-context-array-outcome"},
|
// {"contains(steps.*.outcome, 'failure')", true, "steps-context-array-outcome"},
|
||||||
// {"contains(steps.*.outputs.name, 'value')", true, "steps-context-array-outputs"},
|
// {"contains(steps.*.outputs.name, 'value')", true, "steps-context-array-outputs"},
|
||||||
{"runner.os", "Linux", "runner-context"},
|
{input: "runner.os", expected: "Linux", name: "runner-context"},
|
||||||
{"secrets.name", "value", "secrets-context"},
|
{input: "secrets.name", expected: "value", name: "secrets-context"},
|
||||||
{"vars.name", "value", "vars-context"},
|
{input: "vars.name", expected: "value", name: "vars-context"},
|
||||||
{"strategy.fail-fast", true, "strategy-context"},
|
{input: "strategy.fail-fast", expected: true, name: "strategy-context"},
|
||||||
{"matrix.os", "Linux", "matrix-context"},
|
{input: "matrix.os", expected: "Linux", name: "matrix-context"},
|
||||||
{"needs.job-id.outputs.output-name", "value", "needs-context"},
|
{input: "needs.job-id.outputs.output-name", expected: "value", name: "needs-context"},
|
||||||
{"needs.job-id.result", "success", "needs-context"},
|
{input: "needs.job-id.result", expected: "success", name: "needs-context"},
|
||||||
{"contains(needs.*.result, 'success')", true, "needs-wildcard-context-contains-success"},
|
{input: "contains(needs.*.result, 'success')", expected: true, name: "needs-wildcard-context-contains-success"},
|
||||||
{"contains(needs.*.result, 'failure')", false, "needs-wildcard-context-contains-failure"},
|
{input: "contains(needs.*.result, 'failure')", expected: false, name: "needs-wildcard-context-contains-failure"},
|
||||||
{"inputs.name", "value", "inputs-context"},
|
{input: "inputs.name", expected: "value", name: "inputs-context"},
|
||||||
|
{input: "vars.MY_VAR", expected: "refs/heads/test-data", name: "vars-context", ctxdata: map[string]interface{}{"vars": map[string]interface{}{"MY_VAR": "refs/heads/test-data"}}},
|
||||||
|
{input: "vars.MY_VAR", expected: "refs/heads/test-data", name: "vars-context", ctxdata: map[string]interface{}{"vars": map[string]interface{}{"my_var": "refs/heads/test-data"}}},
|
||||||
}
|
}
|
||||||
|
|
||||||
env := &EvaluationEnvironment{
|
env := EvaluationEnvironment{
|
||||||
Github: &model.GithubContext{
|
Github: &model.GithubContext{
|
||||||
Action: "push",
|
Action: "push",
|
||||||
},
|
},
|
||||||
@@ -626,7 +638,10 @@ func TestContexts(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range table {
|
for _, tt := range table {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
output, err := NewInterpeter(env, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
tenv := env
|
||||||
|
tenv.EnvCS = tt.caseSensitiveEnv
|
||||||
|
tenv.CtxData = tt.ctxdata
|
||||||
|
output, err := NewInterpeter(&tenv, Config{}).Evaluate(tt.input, DefaultStatusCheckNone)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, tt.expected, output)
|
assert.Equal(t, tt.expected, output)
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ func (rc *RunContext) NewExpressionEvaluatorWithEnv(ctx context.Context, env map
|
|||||||
}
|
}
|
||||||
if rc.JobContainer != nil {
|
if rc.JobContainer != nil {
|
||||||
ee.Runner = rc.JobContainer.GetRunnerContext(ctx)
|
ee.Runner = rc.JobContainer.GetRunnerContext(ctx)
|
||||||
|
ee.EnvCS = !rc.JobContainer.IsEnvironmentCaseInsensitive()
|
||||||
}
|
}
|
||||||
return expressionEvaluator{
|
return expressionEvaluator{
|
||||||
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
interpreter: exprparser.NewInterpeter(ee, exprparser.Config{
|
||||||
|
|||||||
Reference in New Issue
Block a user