mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2026-10-24 16:59:19 +03:00
Fixes #1501. As a jobparser upgrade, it requires Runner be released and Forgejo be upgraded to the new version. When a reusable workflow caller has an `if:` condition on the job, it is placed on the expanded jobs that come from it. If an inner job of a reusable workflow also has an `if:` condition, the two conditions are `&&`'d together. I originally planned to evaluate the `if` condition during job parsing, as described in [a comment on #1501](https://code.forgejo.org/forgejo/runner/issues/1501#issuecomment-85822). However, I realized that a common usage for `if` is job-status checks like `if: failed()`, and that cannot be evaluated at expansion time. Manually tested with these workflows: Caller: ``` on: pull_request: jobs: top-true-inner-true: if: true uses: ./.forgejo/workflows/shared/child.yml with: value: first top-true-inner-false: if: true uses: ./.forgejo/workflows/shared/child.yml with: value: not first top-false: if: false uses: ./.forgejo/workflows/shared/child.yml with: value: would be second ``` Child: ``` name: Child workflow on: workflow_call: inputs: value: type: string required: true jobs: child-workflow: runs-on: docker if: ${{ inputs.value == 'first' }} steps: - name: Echo run: echo ${{ inputs.value }} ``` <!--start release-notes-assistant--> <!--URL:https://code.forgejo.org/forgejo/runner--> - features - [PR](https://code.forgejo.org/forgejo/runner/pulls/1509): <!--number 1509 --><!--line 0 --><!--description ZmVhdDogbWVyZ2UgcmV1c2FibGUgZXhwYW5zaW9uIGNhbGxlcidzICdpZicgaW50byBleHBhbmRlZCBqb2Jz-->feat: merge reusable expansion caller's 'if' into expanded jobs<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://code.forgejo.org/forgejo/runner/pulls/1509 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.code.forgejo.org>
226 lines
6.1 KiB
Go
226 lines
6.1 KiB
Go
package exprparser
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rhysd/actionlint"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.yaml.in/yaml/v3"
|
|
)
|
|
|
|
func TestRender(t *testing.T) {
|
|
testCases := []struct {
|
|
input string
|
|
}{
|
|
{input: "'hello'"},
|
|
{input: "123"},
|
|
{input: "123.456"},
|
|
{input: "null"},
|
|
{input: "true"},
|
|
{input: "false"},
|
|
{input: "jobs"},
|
|
{input: "jobs[0]"},
|
|
{input: "jobs.needs.input.thing"},
|
|
{input: "jobs.*"},
|
|
{input: "!false"},
|
|
{input: "1 > 2"},
|
|
{input: "true || false"},
|
|
{input: "somefunc(1, 2, 3)"},
|
|
{input: "somefunc(1)"},
|
|
{input: "somefunc()"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.input, func(t *testing.T) {
|
|
parser := actionlint.NewExprParser()
|
|
exprNode, exprErr := parser.Parse(actionlint.NewExprLexer(tc.input + "}}"))
|
|
require.Nil(t, exprErr)
|
|
|
|
var builder strings.Builder
|
|
err := render(&builder, exprNode)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tc.input, builder.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
// Tests that without any mutation configuration, the mutator can round-trip an expression without change.
|
|
func TestNoopMutator(t *testing.T) {
|
|
testCases := []struct {
|
|
input string
|
|
}{
|
|
{input: "'hello'"},
|
|
{input: "123"},
|
|
{input: "123.456"},
|
|
{input: "null"},
|
|
{input: "true"},
|
|
{input: "false"},
|
|
{input: "jobs"},
|
|
{input: "jobs[0]"},
|
|
{input: "jobs.needs.input.thing"},
|
|
{input: "jobs.*"},
|
|
{input: "!false"},
|
|
{input: "1 > 2"},
|
|
{input: "true || false"},
|
|
{input: "somefunc(1, 2, 3)"},
|
|
{input: "somefunc(1)"},
|
|
{input: "somefunc()"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.input, func(t *testing.T) {
|
|
parser := actionlint.NewExprParser()
|
|
exprNode, exprErr := parser.Parse(actionlint.NewExprLexer(tc.input + "}}"))
|
|
require.Nil(t, exprErr)
|
|
|
|
mutator := &mutator{}
|
|
newExprNode, hasEffect, err := mutator.mutate(exprNode)
|
|
require.NoError(t, err)
|
|
|
|
var builder strings.Builder
|
|
err = render(&builder, newExprNode)
|
|
require.NoError(t, err)
|
|
assert.False(t, hasEffect)
|
|
assert.Equal(t, tc.input, builder.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVariableAccessMutator(t *testing.T) {
|
|
vam := &VariableAccessMutator{
|
|
Variable: "jobs",
|
|
Rewriter: func(property actionlint.ExprNode) actionlint.ExprNode {
|
|
// jobs[property] has been accessed; rewrite to outputs[property]
|
|
return &actionlint.IndexAccessNode{
|
|
Operand: &actionlint.VariableNode{Name: "outputs"},
|
|
Index: property,
|
|
}
|
|
},
|
|
}
|
|
|
|
t.Run("object deref", func(t *testing.T) {
|
|
retval, err := vam.SingleNodeMutation(&actionlint.ObjectDerefNode{
|
|
Receiver: &actionlint.VariableNode{Name: "jobs"},
|
|
Property: "some-job",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
var builder strings.Builder
|
|
err = render(&builder, retval)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "outputs['some-job']", builder.String())
|
|
})
|
|
|
|
t.Run("array deref", func(t *testing.T) {
|
|
retval, err := vam.SingleNodeMutation(&actionlint.IndexAccessNode{
|
|
Operand: &actionlint.VariableNode{Name: "jobs"},
|
|
Index: &actionlint.FuncCallNode{
|
|
Callee: "format",
|
|
Args: []actionlint.ExprNode{
|
|
&actionlint.StringNode{Value: "hello world"},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
var builder strings.Builder
|
|
err = render(&builder, retval)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "outputs[format('hello world')]", builder.String())
|
|
})
|
|
}
|
|
|
|
func TestMutate(t *testing.T) {
|
|
vam1 := &VariableAccessMutator{
|
|
Variable: "variable",
|
|
Rewriter: func(property actionlint.ExprNode) actionlint.ExprNode {
|
|
// jobs[property] has been accessed; rewrite to outputs[property]
|
|
return &actionlint.IndexAccessNode{
|
|
Operand: &actionlint.VariableNode{Name: "outputs"},
|
|
Index: property,
|
|
}
|
|
},
|
|
}
|
|
vam2 := &VariableAccessMutator{
|
|
Variable: "constant",
|
|
Rewriter: func(property actionlint.ExprNode) actionlint.ExprNode {
|
|
// jobs[property] has been accessed; rewrite to outputs[property]
|
|
return &actionlint.IndexAccessNode{
|
|
Operand: &actionlint.VariableNode{Name: "forgejo"},
|
|
Index: property,
|
|
}
|
|
},
|
|
}
|
|
|
|
output, err := Mutate("Hello ${{ variable.content }}, welcome to ${{ constant.real-world }}.", vam1, vam2)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "${{ format('Hello {0}, welcome to {1}.', outputs['content'], forgejo['real-world']) }}", output)
|
|
|
|
// Verify that a string with no mutations performed is returned identical to input.
|
|
input := "Hello ${{ vars.content }}, welcome to ${{ consts.real-world }}."
|
|
output, err = Mutate(input, vam1, vam2)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, input, output)
|
|
}
|
|
|
|
func TestMutateYamlNode(t *testing.T) {
|
|
testCases := []struct {
|
|
input string
|
|
output string
|
|
}{
|
|
{
|
|
input: "hello ${{ var.world }}",
|
|
output: "${{ format('hello {0}', rewritten-var['world']) }}\n",
|
|
},
|
|
{
|
|
input: "3.1415926",
|
|
output: "3.1415926\n",
|
|
},
|
|
{
|
|
input: "- hello ${{ var.world }}\n- goodbye, ${{ var.something }}\n",
|
|
output: "- ${{ format('hello {0}', rewritten-var['world']) }}\n- ${{ format('goodbye, {0}', rewritten-var['something']) }}\n",
|
|
},
|
|
{
|
|
input: "key: hello ${{ var.world }}\n",
|
|
output: "key: ${{ format('hello {0}', rewritten-var['world']) }}\n",
|
|
},
|
|
// `jobs.<job_id>.if` and `jobs.<job_id>.steps[*].if` in workflows have a special behaviour -- the `${{ ... }}`
|
|
// wrapper can be omitted. `MutateYamlNode` implements this by automatically wrapping nodes with a key `if`.
|
|
{
|
|
input: "if: ${{ var.world }}\n",
|
|
output: "if: ${{ rewritten-var['world'] }}\n",
|
|
},
|
|
{
|
|
input: "if: var.world\n",
|
|
output: "if: ${{ rewritten-var['world'] }}\n",
|
|
},
|
|
}
|
|
|
|
vam := &VariableAccessMutator{
|
|
Variable: "var",
|
|
Rewriter: func(property actionlint.ExprNode) actionlint.ExprNode {
|
|
return &actionlint.IndexAccessNode{
|
|
Operand: &actionlint.VariableNode{Name: "rewritten-var"},
|
|
Index: property,
|
|
}
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.input, func(t *testing.T) {
|
|
var node yaml.Node
|
|
err := yaml.Unmarshal([]byte(tc.input), &node)
|
|
require.NoError(t, err)
|
|
|
|
err = MutateYamlNode(node.Content[0], vam)
|
|
require.NoError(t, err)
|
|
|
|
myYaml, err := yaml.Marshal(node.Content[0])
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tc.output, string(myYaml))
|
|
})
|
|
}
|
|
}
|