mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-08-07 17:34:22 +02:00
fix: evaluate each ${{ }} part on its own (#1146)
Every `${{ }}` part was spliced as raw text into a synthesized `format('...', <raw>)` call and re-parsed, so unbalanced parentheses restructured the whole expression:
```yaml
run: echo ${{ 1) && (2 }} # panicked with "did not evaluate to a string"
if: ${{ 1 }} ${{ 0) && (0 }} # silently skipped the step
```
One scanner shaped like GitHub's template reader now splits every value, and each part is evaluated on its own, so nothing builds an expression out of text.
An empty input to `Evaluate` asked `success()` whatever the caller requested, so a post step running under `always()` asked the wrong question.
Same fix as https://github.com/go-gitea/gitea/pull/38754 on the Gitea side.
Written by Claude Opus 5.
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1146
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -259,6 +259,15 @@ func TestInterpolate(t *testing.T) {
|
||||
{"${{ env.SOMETHING_FALSE || false }}", "false"},
|
||||
{"${{ env.SOMETHING_FALSE }} && ${{ env.SOMETHING_TRUE }}", "false && true"},
|
||||
{"${{ fromJSON('{}') < 2 }}", "false"},
|
||||
{"${{ 1 }}", "1"},
|
||||
{"${{ 1.0 }}", "1"},
|
||||
{"${{ null }}", ""},
|
||||
{"${{ fromJSON('[1,2]') }}", "Array"},
|
||||
{"${{ fromJSON('{\"a\":1}') }}", "Object"},
|
||||
// a malformed part must not restructure its neighbours, and it interpolates to nothing
|
||||
{"${{ 1) && (2 }}", ""},
|
||||
{"run ${{ 1) && (2 }} now", ""},
|
||||
{"${{ 1", "${{ 1"},
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
@@ -270,57 +279,38 @@ func TestInterpolate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteSubExpression(t *testing.T) {
|
||||
table := []struct {
|
||||
in string
|
||||
out string
|
||||
func TestSplitSubExpressions(t *testing.T) {
|
||||
expr := func(text string) exprPart { return exprPart{text: text, isExpr: true} }
|
||||
literal := func(text string) exprPart { return exprPart{text: text} }
|
||||
|
||||
for _, tt := range []struct {
|
||||
in string
|
||||
want []exprPart
|
||||
}{
|
||||
{in: "Hello World", out: "Hello World"},
|
||||
{in: "${{ true }}", out: "${{ true }}"},
|
||||
{in: "${{ true }} ${{ true }}", out: "format('{0} {1}', true, true)"},
|
||||
{in: "${{ true || false }} ${{ true && true }}", out: "format('{0} {1}', true || false, true && true)"},
|
||||
{in: "${{ '}}' }}", out: "${{ '}}' }}"},
|
||||
{in: "${{ '''}}''' }}", out: "${{ '''}}''' }}"},
|
||||
{in: "${{ '''' }}", out: "${{ '''' }}"},
|
||||
{in: `${{ fromJSON('"}}"') }}`, out: `${{ fromJSON('"}}"') }}`},
|
||||
{in: `${{ fromJSON('"\"}}\""') }}`, out: `${{ fromJSON('"\"}}\""') }}`},
|
||||
{in: `${{ fromJSON('"''}}"') }}`, out: `${{ fromJSON('"''}}"') }}`},
|
||||
{in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"},
|
||||
{"Hello World", []exprPart{literal("Hello World")}},
|
||||
{"${{ true }}", []exprPart{expr("true")}},
|
||||
{"${{ true }} ${{ false }}", []exprPart{expr("true"), literal(" "), expr("false")}},
|
||||
{"Hello ${{ 'World' }}", []exprPart{literal("Hello "), expr("'World'")}},
|
||||
// a quote toggles string state, so a `}}` inside a string does not end the expression
|
||||
{"${{ '}}' }}", []exprPart{expr("'}}'")}},
|
||||
{"${{ '''}}''' }}", []exprPart{expr("'''}}'''")}},
|
||||
{"${{ '''' }}", []exprPart{expr("''''")}},
|
||||
{`${{ fromJSON('"}}"') }}`, []exprPart{expr(`fromJSON('"}}"')`)}},
|
||||
{`${{ fromJSON('"\"}}\""') }}`, []exprPart{expr(`fromJSON('"\"}}\""')`)}},
|
||||
{`${{ fromJSON('"''}}"') }}`, []exprPart{expr(`fromJSON('"''}}"')`)}},
|
||||
// without a complete literal the value stays text, as GitHub's template reader leaves it
|
||||
{"${{ 1", []exprPart{literal("${{ 1")}},
|
||||
// a malformed part stays one part, so it cannot restructure its neighbours
|
||||
{"${{ 1) && (2 }}", []exprPart{expr("1) && (2")}},
|
||||
} {
|
||||
got, err := splitSubExpressions(tt.in)
|
||||
require.NoError(t, err, tt.in)
|
||||
assert.Equal(t, tt.want, got, tt.in)
|
||||
}
|
||||
|
||||
for _, table := range table {
|
||||
t.Run("TestRewriteSubExpression", func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
out, err := rewriteSubExpression(context.Background(), table.in, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteSubExpressionForceFormat(t *testing.T) {
|
||||
table := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{in: "Hello World", out: "Hello World"},
|
||||
{in: "${{ true }}", out: "format('{0}', true)"},
|
||||
{in: "${{ '}}' }}", out: "format('{0}', '}}')"},
|
||||
{in: `${{ fromJSON('"}}"') }}`, out: `format('{0}', fromJSON('"}}"'))`},
|
||||
{in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"},
|
||||
}
|
||||
|
||||
for _, table := range table {
|
||||
t.Run("TestRewriteSubExpressionForceFormat", func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
out, err := rewriteSubExpression(context.Background(), table.in, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
})
|
||||
for _, in := range []string{"${{ 'a' }} ${{ b", "${{ 'a }}"} {
|
||||
_, err := splitSubExpressions(in)
|
||||
assert.ErrorContains(t, err, "unclosed expression", in)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user