Improve service syntax related docs and tests nits (#5991)

This commit is contained in:
6543
2026-01-19 18:44:18 +01:00
committed by GitHub
parent a16eb1152c
commit d1b7e35ca8
6 changed files with 180 additions and 105 deletions

View File

@@ -46,8 +46,8 @@ Service containers generally expose environment variables to customize service s
- name: database
image: mysql
+ environment:
+ - MYSQL_DATABASE=test
+ - MYSQL_ALLOW_EMPTY_PASSWORD=yes
+ MYSQL_DATABASE: test
+ MYSQL_ALLOW_EMPTY_PASSWORD: yes
- name: cache
image: redis
@@ -102,8 +102,8 @@ services:
- name: database
image: mysql
environment:
- MYSQL_DATABASE=test
- MYSQL_ROOT_PASSWORD=example
MYSQL_DATABASE: test
MYSQL_ROOT_PASSWORD: example
steps:
- name: get-version
image: ubuntu

View File

@@ -18,6 +18,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/errors"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml"
@@ -185,7 +186,7 @@ func TestLintErrors(t *testing.T) {
for _, test := range testdata {
conf, err := yaml.ParseString(test.from)
assert.NoError(t, err)
require.NoError(t, err)
lerr := linter.New().Lint([]*linter.WorkflowConfig{{
File: test.from,

View File

@@ -8,6 +8,14 @@ steps:
services:
database:
image: mysql
ports:
- 3306
environment:
MYSQL_DATABASE: test
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
cache:
image: redis
directory: /tmp/
ports:
- '6379'

View File

@@ -183,7 +183,6 @@ steps:
- go test
build:
image: golang
network_mode: container:name
commands:
- go build
when:
@@ -191,7 +190,8 @@ steps:
depends_on: []
notify:
image: slack
channel: dev
settings:
channel: dev
when:
event: failure
services:
@@ -252,7 +252,7 @@ func TestReSerialize(t *testing.T) {
t.Fail()
}
workBin, err := yaml.Marshal(work1)
work1Bin, err := yaml.Marshal(work1)
if !assert.NoError(t, err) {
t.Fail()
}
@@ -282,7 +282,58 @@ func TestReSerialize(t *testing.T) {
DRIVER: next
PLATFORM: linux
skip_clone: false
`, string(workBin))
`, string(work1Bin))
work2, err := ParseString(sampleYaml)
if !assert.NoError(t, err) {
t.Fail()
}
workBin2, err := yaml.Marshal(work2)
if !assert.NoError(t, err) {
t.Fail()
}
// TODO: fix "steps.[1].depends_on: []" to be re-serialized!
assert.EqualValues(t, `when:
- event:
- tester
- tester2
- branch: tester
workspace:
base: /go
path: src/github.com/octocat/hello-world
steps:
- name: test
image: golang
commands:
- go install
- go test
- name: build
image: golang
commands: go build
when:
event: push
- name: notify
image: slack
settings:
channel: dev
when:
event: failure
services:
- name: database
image: mysql
labels:
com.example.team: frontend
com.example.type: build
depends_on:
- lint
- test
runs_on:
- success
- failure
skip_clone: false
`, string(workBin2))
}
func TestSlice(t *testing.T) {

View File

@@ -15,110 +15,46 @@
package types
import (
"fmt"
"gopkg.in/yaml.v3"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/constraint"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types/base"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/utils"
)
type (
// ContainerList denotes an ordered collection of containers.
ContainerList struct {
ContainerList []*Container
}
// Container defines a container.
type Container struct {
// common
Name string `yaml:"name,omitempty"`
Image string `yaml:"image,omitempty"`
Pull bool `yaml:"pull,omitempty"`
Commands base.StringOrSlice `yaml:"commands,omitempty"`
Entrypoint base.StringOrSlice `yaml:"entrypoint,omitempty"`
Directory string `yaml:"directory,omitempty"`
Settings map[string]any `yaml:"settings,omitempty"`
Environment map[string]any `yaml:"environment,omitempty"`
// flow control
DependsOn base.StringOrSlice `yaml:"depends_on,omitempty"`
When constraint.When `yaml:"when,omitempty"`
Failure string `yaml:"failure,omitempty"`
Detached bool `yaml:"detach,omitempty"`
// state
Volumes Volumes `yaml:"volumes,omitempty"`
// network
Ports []string `yaml:"ports,omitempty"`
DNS base.StringOrSlice `yaml:"dns,omitempty"`
DNSSearch base.StringOrSlice `yaml:"dns_search,omitempty"`
// backend specific
BackendOptions map[string]any `yaml:"backend_options,omitempty"`
// Container defines a container.
Container struct {
// common
Name string `yaml:"name,omitempty"`
Image string `yaml:"image,omitempty"`
Pull bool `yaml:"pull,omitempty"`
Commands base.StringOrSlice `yaml:"commands,omitempty"`
Entrypoint base.StringOrSlice `yaml:"entrypoint,omitempty"`
Directory string `yaml:"directory,omitempty"`
Settings map[string]any `yaml:"settings,omitempty"`
Environment map[string]any `yaml:"environment,omitempty"`
// flow control
DependsOn base.StringOrSlice `yaml:"depends_on,omitempty"`
When constraint.When `yaml:"when,omitempty"`
Failure string `yaml:"failure,omitempty"`
Detached bool `yaml:"detach,omitempty"`
// state
Volumes Volumes `yaml:"volumes,omitempty"`
// network
Ports []string `yaml:"ports,omitempty"`
DNS base.StringOrSlice `yaml:"dns,omitempty"`
DNSSearch base.StringOrSlice `yaml:"dns_search,omitempty"`
// backend specific
BackendOptions map[string]any `yaml:"backend_options,omitempty"`
// ACTIVE DEVELOPMENT BELOW
// ACTIVE DEVELOPMENT BELOW
// Docker and Kubernetes Specific
Privileged bool `yaml:"privileged,omitempty"`
// Docker and Kubernetes Specific
Privileged bool `yaml:"privileged,omitempty"`
// Undocumented
Devices []string `yaml:"devices,omitempty"`
ExtraHosts []string `yaml:"extra_hosts,omitempty"`
NetworkMode string `yaml:"network_mode,omitempty"`
Tmpfs []string `yaml:"tmpfs,omitempty"`
}
)
// UnmarshalYAML implements the Unmarshaler interface.
func (c *ContainerList) UnmarshalYAML(value *yaml.Node) error {
switch value.Kind {
// We support maps ...
case yaml.MappingNode:
c.ContainerList = make([]*Container, 0, len(value.Content)/2+1)
// We cannot use decode on specific values
// since if we try to load from a map, the order
// will not be kept. Therefor use value.Content
// and take the map values i%2=1
for i, n := range value.Content {
if i%2 == 1 {
container := &Container{}
if err := n.Decode(container); err != nil {
return err
}
if container.Name == "" {
container.Name = fmt.Sprintf("%v", value.Content[i-1].Value)
}
c.ContainerList = append(c.ContainerList, container)
}
}
// ... and lists
case yaml.SequenceNode:
c.ContainerList = make([]*Container, 0, len(value.Content))
for i, n := range value.Content {
container := &Container{}
if err := n.Decode(container); err != nil {
return err
}
if container.Name == "" {
container.Name = fmt.Sprintf("step-%d", i)
}
c.ContainerList = append(c.ContainerList, container)
}
default:
return fmt.Errorf("yaml node type[%d]: '%s' not supported", value.Kind, value.Tag)
}
return nil
}
// MarshalYAML implements custom Yaml marshaling.
func (c ContainerList) MarshalYAML() (any, error) {
return c.ContainerList, nil
// Undocumented
Devices []string `yaml:"devices,omitempty"`
ExtraHosts []string `yaml:"extra_hosts,omitempty"`
NetworkMode string `yaml:"network_mode,omitempty"`
Tmpfs []string `yaml:"tmpfs,omitempty"`
}
func (c *Container) IsPlugin() bool {

View File

@@ -0,0 +1,79 @@
// Copyright 2026 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import (
"fmt"
"gopkg.in/yaml.v3"
)
// ContainerList contains ordered collection of containers.
type ContainerList struct {
ContainerList []*Container
}
// UnmarshalYAML implements the Unmarshaler interface.
func (c *ContainerList) UnmarshalYAML(value *yaml.Node) error {
switch value.Kind {
// We support maps ...
case yaml.MappingNode:
c.ContainerList = make([]*Container, 0, len(value.Content)/2+1)
// We cannot use decode on specific values
// since if we try to load from a map, the order
// will not be kept. Therefor use value.Content
// and take the map values i%2=1
for i, n := range value.Content {
if i%2 == 1 {
container := &Container{}
if err := n.Decode(container); err != nil {
return err
}
if container.Name == "" {
container.Name = fmt.Sprintf("%v", value.Content[i-1].Value)
}
c.ContainerList = append(c.ContainerList, container)
}
}
// ... and lists
case yaml.SequenceNode:
c.ContainerList = make([]*Container, 0, len(value.Content))
for i, n := range value.Content {
container := &Container{}
if err := n.Decode(container); err != nil {
return err
}
if container.Name == "" {
container.Name = fmt.Sprintf("step-%d", i)
}
c.ContainerList = append(c.ContainerList, container)
}
default:
return fmt.Errorf("yaml node type[%d]: '%s' not supported", value.Kind, value.Tag)
}
return nil
}
// MarshalYAML implements custom Yaml marshaling.
func (c ContainerList) MarshalYAML() (any, error) {
return c.ContainerList, nil
}