mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-08 14:47:44 +00:00
00a637295e
Backport of https://github.com/go-gitea/gitea/pull/38754 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-name: ${{ 1) && (2 }} # panicked, aborting workflow parsing for the push if: ${{ 1 }} ${{ 0) && (0 }} # silently skipped the job runs-on: ${{ nosuchcontext.x }} # silently queued the job against the label "" ``` 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. A part that fails is an error instead of an empty string. `expressionCallsFunction` is self-contained here, since this branch has no `expressionsMatch` to build it on. That makes `github.com/rhysd/actionlint` a direct dependency, which it already is on `main`.
150 lines
3.6 KiB
Go
150 lines
3.6 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package jobparser
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.com/gitea/runner/act/model"
|
|
"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,
|
|
},
|
|
{
|
|
name: "continue_on_error_expr",
|
|
options: nil,
|
|
wantErr: false,
|
|
},
|
|
}
|
|
invalidFileTests := []struct {
|
|
name string
|
|
}{
|
|
{name: "null_job_implicit"},
|
|
{name: "null_job_explicit"},
|
|
}
|
|
|
|
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())
|
|
})
|
|
}
|
|
|
|
for _, tt := range invalidFileTests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
content := ReadTestdata(t, tt.name+".in.yaml")
|
|
require.NotPanics(t, func() {
|
|
_, err := Parse(content)
|
|
require.Error(t, err)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseInterpolatesRunName(t *testing.T) {
|
|
workflow := func(runName string) []byte {
|
|
return []byte("name: t\nrun-name: \"" + runName + "\"\non: push\njobs:\n build:\n runs-on: ubuntu-latest\n steps: [{run: echo}]\n")
|
|
}
|
|
|
|
for _, tt := range []struct{ name, runName, want string }{
|
|
{"bool", "${{ true }}", "true"},
|
|
{"int", "${{ 1 }}", "1"},
|
|
{"float", "${{ 1.0 }}", "1"},
|
|
{"null", "${{ null }}", ""},
|
|
{"object", `${{ fromJSON('{\"a\":1}') }}`, "Object"},
|
|
{"array", "${{ fromJSON('[1,2]') }}", "Array"},
|
|
{"context", "${{ github }}", "Object"},
|
|
{"surrounding literals", "run ${{ 1 }} now", "run 1 now"},
|
|
{"two expressions", "${{ 1 }}-${{ true }}", "1-true"},
|
|
{"closing brace inside a string", "${{ 'a}}b' }}", "a}}b"},
|
|
{"incomplete expression stays literal", "${{ 1", "${{ 1"},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := Parse(workflow(tt.runName), WithGitContext(&model.GithubContext{EventName: "push"}))
|
|
require.NoError(t, err)
|
|
require.Len(t, result, 1)
|
|
assert.Equal(t, tt.want, result[0].RunName)
|
|
})
|
|
}
|
|
|
|
// a malformed part must not restructure the surrounding expression
|
|
for _, runName := range []string{"${{ 1) && (2 }}", "run ${{ 1) && (2 }} now", "${{ 'a' }} ${{ b", "${{ 'a }}"} {
|
|
_, err := Parse(workflow(runName), WithGitContext(&model.GithubContext{EventName: "push"}))
|
|
assert.ErrorContains(t, err, "interpolate run-name")
|
|
}
|
|
|
|
// callers such as commit status parse without a git context, leaving `github` a nil pointer
|
|
result, err := Parse(workflow("${{ github }}"))
|
|
require.NoError(t, err)
|
|
require.Len(t, result, 1)
|
|
assert.Empty(t, result[0].RunName)
|
|
}
|