Skip to content
12 changes: 6 additions & 6 deletions docs/docs/20-usage/60-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ steps:
- go test

services:
- name: database
database:
image: mysql

- name: cache
cache:
image: redis
```

You can define a port and a protocol explicitly:

```yaml
services:
- name: database
database:
image: mysql
ports:
- 3306

- name: wireguard
wireguard:
image: wg
ports:
- 51820/udp
Expand All @@ -43,7 +43,7 @@ Service containers generally expose environment variables to customize service s

```diff
services:
- name: database
database:
image: mysql
+ environment:
+ MYSQL_DATABASE: test
Expand Down Expand Up @@ -91,7 +91,7 @@ Service containers require time to initialize and begin to accept connections. I
- go test

services:
- name: database
database:
image: mysql
```

Expand Down
4 changes: 2 additions & 2 deletions pipeline/frontend/yaml/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,10 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
}

// add services steps
if len(conf.Services.ContainerList) != 0 {
if len(conf.Services.ContainerMap) != 0 {
stage := new(backend_types.Stage)

for _, container := range conf.Services.ContainerList {
for _, container := range conf.Services.ContainerMap {
if match, err := container.When.Match(c.metadata, false, c.env); !match && err == nil {
continue
} else if err != nil {
Expand Down
16 changes: 15 additions & 1 deletion pipeline/frontend/yaml/linter/linter.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,21 @@ func (l *Linter) lintContainers(config *WorkflowConfig, area string) error {
case "steps":
containers = config.Workflow.Steps.ContainerList
case "services":
containers = config.Workflow.Services.ContainerList
if len(config.Workflow.Services.Duplicated) != 0 {
linterErr = multierr.Append(linterErr, &errorTypes.PipelineError{
Type: errorTypes.PipelineErrorTypeBadHabit,
Message: "Services have duplicated names declaired, second ones are ignored and should be removed",
Data: errors.BadHabitErrorData{
File: config.File,
Field: "services",
Docs: "https://woodpecker-ci.org/docs/usage/services",
},
IsWarning: true,
})
}
for _, v := range config.Workflow.Services.ContainerMap {
containers = append(containers, v)
}
}

for _, container := range containers {
Expand Down
11 changes: 7 additions & 4 deletions pipeline/frontend/yaml/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ func TestParse(t *testing.T) {

assert.Equal(t, "/go", out.Workspace.Base)
assert.Equal(t, "src/github.com/octocat/hello-world", out.Workspace.Path)
assert.Equal(t, "database", out.Services.ContainerList[0].Name)
assert.Equal(t, "mysql", out.Services.ContainerList[0].Image)
for k := range out.Services.ContainerMap {
assert.Equal(t, k, out.Services.ContainerMap[k].Name)
}
assert.Len(t, out.Services.ContainerMap, 1)
assert.Equal(t, "mysql", out.Services.ContainerMap["database"].Image)
assert.Equal(t, "test", out.Steps.ContainerList[0].Name)
assert.Equal(t, "golang", out.Steps.ContainerList[0].Image)
assert.Equal(t, yaml_base_types.StringOrSlice{"go install", "go test"}, out.Steps.ContainerList[0].Commands)
Expand Down Expand Up @@ -309,8 +312,8 @@ steps:
when:
event: failure
services:
- name: database
image: mysql
database:
image: mysql
labels:
com.example.team: frontend
com.example.type: build
Expand Down
85 changes: 85 additions & 0 deletions pipeline/frontend/yaml/types/container_map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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"
)

// ContainerMap contains collection of containers.
type ContainerMap struct {
ContainerMap map[string]*Container
Duplicated []string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to put this into the parsing code? I think the linter should check separately for duplicated entries and for parsing just use the ContainerList

}

// UnmarshalYAML implements the Unmarshaler interface.
func (c *ContainerMap) UnmarshalYAML(value *yaml.Node) error {
c.ContainerMap = make(map[string]*Container, len(value.Content)/2+1)
switch value.Kind {
// We support maps ...
case yaml.MappingNode:
for i, n := range value.Content {
if i%2 == 1 {
container := &Container{}
if err := n.Decode(container); err != nil {
return err
}

// service name is host name so we set it as name
container.Name = fmt.Sprintf("%v", value.Content[i-1].Value)
if container.Name == "" {
return fmt.Errorf("container map does not allow empty key item")
}

c.ContainerMap[container.Name] = container
}
}

// ... and lists
case yaml.SequenceNode:
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)
}

if _, exist := c.ContainerMap[container.Name]; exist {
c.Duplicated = append(c.Duplicated, container.Name)
} else {
c.ContainerMap[container.Name] = 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 ContainerMap) MarshalYAML() (any, error) {
// we just relay on the map key for names
for _, v := range c.ContainerMap {
v.Name = ""
}
return c.ContainerMap, nil
}
2 changes: 1 addition & 1 deletion pipeline/frontend/yaml/types/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type (
Workspace Workspace `yaml:"workspace,omitempty"`
Clone ContainerList `yaml:"clone,omitempty"`
Steps ContainerList `yaml:"steps,omitempty"`
Services ContainerList `yaml:"services,omitempty"`
Services ContainerMap `yaml:"services,omitempty"`
Labels map[string]string `yaml:"labels,omitempty"`
DependsOn []string `yaml:"depends_on,omitempty"`
RunsOn []string `yaml:"runs_on,omitempty"`
Expand Down