chore: initialize Gitea v1.27.2 fork
giteabot backport / giteabot (push) Canceled after 0s
giteabot / giteabot (push) Canceled after 0s
release-nightly / nightly-binary (push) Canceled after 0s
release-nightly / nightly-container (push) Canceled after 0s
cache-seeder / gobuild (push) Canceled after 0s
cache-seeder / lint (bindata, lint-backend) (push) Canceled after 0s
release-nightly-snapcraft / build-and-publish (push) Canceled after 0s
giteabot backport / giteabot (push) Canceled after 0s
giteabot / giteabot (push) Canceled after 0s
release-nightly / nightly-binary (push) Canceled after 0s
release-nightly / nightly-container (push) Canceled after 0s
cache-seeder / gobuild (push) Canceled after 0s
cache-seeder / lint (bindata, lint-backend) (push) Canceled after 0s
release-nightly-snapcraft / build-and-publish (push) Canceled after 0s
Includes direct password setup links in registration emails. Assisted-by: Codex:GPT-5
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package jobparser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/act/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",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// `workflow_call` is only fired by another workflow's `uses:`, so ParseRawOn intentionally excludes it from trigger detection.
|
||||
input: `on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
env:
|
||||
type: string
|
||||
required: true
|
||||
outputs:
|
||||
sha:
|
||||
value: ${{ jobs.build.outputs.commit }}
|
||||
secrets:
|
||||
DEPLOY_KEY:
|
||||
required: true
|
||||
`,
|
||||
result: []*Event{},
|
||||
},
|
||||
{
|
||||
// Mixed: a workflow that is both callable AND triggered by push. Only the "push" event surfaces.
|
||||
input: `on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
env:
|
||||
type: string
|
||||
push:
|
||||
branches: [main]
|
||||
`,
|
||||
result: []*Event{
|
||||
{
|
||||
Name: "push",
|
||||
acts: map[string][]string{"branches": {"main"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Scalar form: a purely reusable workflow has no event triggers.
|
||||
input: "on: workflow_call",
|
||||
result: []*Event{},
|
||||
},
|
||||
{
|
||||
// Sequence form: `workflow_call` is excluded while sibling events are kept.
|
||||
input: "on:\n - push\n - workflow_call\n - pull_request",
|
||||
result: []*Event{
|
||||
{Name: "push"},
|
||||
{Name: "pull_request"},
|
||||
},
|
||||
},
|
||||
}
|
||||
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 TestGetContinueOnError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
yaml string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "absent",
|
||||
yaml: "name: test\non: push\njobs:\n job1:\n runs-on: ubuntu-22.04\n steps:\n - run: echo hi\n",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "static true",
|
||||
yaml: "name: test\non: push\njobs:\n job1:\n runs-on: ubuntu-22.04\n continue-on-error: true\n steps:\n - run: echo hi\n",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "static false",
|
||||
yaml: "name: test\non: push\njobs:\n job1:\n runs-on: ubuntu-22.04\n continue-on-error: false\n steps:\n - run: echo hi\n",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Parse([]byte(tt.yaml))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 1)
|
||||
_, job := got[0].Job()
|
||||
assert.Equal(t, tt.want, job.GetContinueOnError())
|
||||
})
|
||||
}
|
||||
|
||||
// Expression case: ${{ matrix.experimental }} must resolve per matrix variant.
|
||||
t.Run("matrix expression", func(t *testing.T) {
|
||||
content := ReadTestdata(t, "continue_on_error_expr.in.yaml")
|
||||
got, err := Parse(content)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
// sorted by matrix name: (false) before (true)
|
||||
_, jobFalse := got[0].Job()
|
||||
_, jobTrue := got[1].Job()
|
||||
assert.False(t, jobFalse.GetContinueOnError())
|
||||
assert.True(t, jobTrue.GetContinueOnError())
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateJobIfExpressionMatrix(t *testing.T) {
|
||||
ifExprs := []string{
|
||||
`${{ contains(fromJSON('["linux","windows"]'), matrix.target) }}`,
|
||||
`${{ contains('["linux","windows"]', matrix.target) }}`,
|
||||
}
|
||||
|
||||
want := map[string]bool{
|
||||
"build (linux)": true,
|
||||
"build (windows)": true,
|
||||
"build (macos)": false,
|
||||
}
|
||||
|
||||
for _, ifExpr := range ifExprs {
|
||||
t.Run(ifExpr, func(t *testing.T) {
|
||||
content := fmt.Sprintf(`
|
||||
name: test
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: %s
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: [linux, windows, macos]
|
||||
steps:
|
||||
- run: echo ${{ matrix.target }}
|
||||
`, ifExpr)
|
||||
|
||||
swfs, err := Parse([]byte(content))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, swfs, 3)
|
||||
|
||||
got := make(map[string]bool, len(swfs))
|
||||
for _, swf := range swfs {
|
||||
id, job := swf.Job()
|
||||
shouldRun, err := EvaluateJobIfExpression(id, job, map[string]any{}, map[string]*JobResult{id: {}}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
got[job.Name] = shouldRun
|
||||
}
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateJobIfExpression(t *testing.T) {
|
||||
kases := []struct {
|
||||
name string
|
||||
ifCond string
|
||||
needResult string
|
||||
expected bool
|
||||
}{
|
||||
{name: "empty need success", ifCond: "${{ 1 == 1 }}", needResult: "success", expected: true},
|
||||
{name: "always", ifCond: "${{ always() }}", needResult: "failure", expected: true},
|
||||
{name: "failure true", ifCond: "${{ failure() }}", needResult: "failure", expected: true},
|
||||
{name: "failure false", ifCond: "${{ failure() }}", needResult: "success", expected: false},
|
||||
{name: "success true", ifCond: "${{ success() }}", needResult: "success", expected: true},
|
||||
// cancelled() is always false on the server: a cancelled run never evaluates a blocked job's `if:`
|
||||
{name: "cancelled", ifCond: "${{ cancelled() }}", needResult: "success", expected: false},
|
||||
{name: "not cancelled or failure", ifCond: "${{ !(cancelled() || failure()) }}", needResult: "success", expected: true},
|
||||
{name: "not cancelled or failure, need failed", ifCond: "${{ !(cancelled() || failure()) }}", needResult: "failure", expected: false},
|
||||
// a condition is an expression with or without `${{ }}`, literal text around one makes it a string
|
||||
{name: "bare expression", ifCond: "always()", needResult: "failure", expected: true},
|
||||
{name: "literal text keeps the success() default", ifCond: "x ${{ 1 }}", needResult: "failure", expected: false},
|
||||
{name: "literal text around a status function drops it", ifCond: "x ${{ always() }}", needResult: "failure", expected: true},
|
||||
}
|
||||
for _, kase := range kases {
|
||||
t.Run(kase.name, func(t *testing.T) {
|
||||
content := strings.ReplaceAll(`
|
||||
name: test
|
||||
on: push
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job1]
|
||||
if: IF_COND
|
||||
steps:
|
||||
- run: echo job2
|
||||
`, "IF_COND", kase.ifCond)
|
||||
|
||||
workflows, err := Parse([]byte(content))
|
||||
require.NoError(t, err)
|
||||
|
||||
var job2 *Job
|
||||
for _, wf := range workflows {
|
||||
if id, job := wf.Job(); id == "job2" {
|
||||
job2 = job
|
||||
}
|
||||
}
|
||||
require.NotNil(t, job2)
|
||||
|
||||
// mirrors findJobNeedsAndFillJobResults: the needs' results plus a self entry carrying Needs
|
||||
results := map[string]*JobResult{
|
||||
"job1": {Result: kase.needResult},
|
||||
"job2": {Needs: []string{"job1"}},
|
||||
}
|
||||
got, err := EvaluateJobIfExpression("job2", job2, map[string]any{}, results, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, kase.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user