Files
werkator/docs/prs/2026-10-04-PR#112-build-definition-applied-in-blocks.md
miandmhoennig 52f42f850e
werkator build succeeded after 01:52
werkator/deploy build succeeded after 00:07
Build definition laid over its branch in three blocks: commands and logs, rules, sandbox (v1.34.9) (#112)
## The Problem

After PR#111 `BuildDefinition.applyTo` had the highest Mutation-CRAP score of Werkator: 34 at a complexity of 34.
A score is never below the complexity, so no test could bring it under the limit of 15.

The method laid a build definition over its branch's settings in one `copy` of 21 fields, each with its own `?:` fallback, the docker and werkdock blocks nested inside.
What the definition says falls into three groups, but the method did not show them:
what the build runs, the rules the instance runs it under, and the sandbox it runs in.
Only one test checked a single field of it (Scenario#72.01); every other field was held only by tests of the config loader.

## Non-Goals

- No change in behaviour: which settings a definition can say, how they inherit, and which are pinned stay as they are.

## The Scenarios

### Feature: A build definition laid over its branch's settings

#### Scenario#112.01: A definition that says nothing runs the branch's settings unchanged

So that `builds.<name>` with only a trigger builds exactly what the branch describes.

- **Given** a branch with a non-default value for every setting
- **When** an empty definition, or one with empty docker and werkdock blocks, is applied
- **Then** the result equals the branch's settings

##### Verified by

- [BuildDefinitionUnitTest](../../src/test/kotlin/de/hoennig/werkator/config/BuildDefinitionUnitTest.kt) — Scenario#112.01

#### Scenario#112.02: Every setting a definition says replaces the branch's own

So that no key of `builds.<name>` is read and then silently dropped.

- **Given** the same branch, and a definition saying every setting, docker and werkdock included, with other values
- **When** the definition is applied
- **Then** every setting is the definition's

##### Verified by

- [BuildDefinitionUnitTest](../../src/test/kotlin/de/hoennig/werkator/config/BuildDefinitionUnitTest.kt) — Scenario#112.02

#### Scenario#112.03: A definition saying some settings of a block keeps the rest of that block

So that `docker.image` alone does not reset the branch's network, socket or environment.

- **Given** the same branch, and a definition saying only the timeout, the docker image and the werkdock rootfs
- **When** the definition is applied
- **Then** those three are the definition's, everything else is the branch's

##### Verified by

- [BuildDefinitionUnitTest](../../src/test/kotlin/de/hoennig/werkator/config/BuildDefinitionUnitTest.kt) — Scenario#112.03

## The Solution

`applyTo` lays the definition over the branch in three blocks:

| Block         | Settings                                                                 |
| ------------- | ------------------------------------------------------------------------ |
| `withWork`    | `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`, `stderrLog` |
| `withRules`   | `requirePullRequest`, `statusContext`, `timeout`, `restartsInstance`     |
| `withSandbox` | `docker`, `werkdock`                                                     |

`DockerOverrides.applyTo` and `WerkdockOverrides.applyTo` lay each sandbox's overrides over the branch's sandbox of that kind.
The overrides and their config class now stand side by side, where Scenario#72.03 already holds that every setting of the one exists in the other.

Measured with PIT as the nightly build runs it:

| Method                        | Before | After      |
| ----------------------------- | ------ | ---------- |
| `BuildDefinition.applyTo`     | 34     | no mutants |
| `BuildDefinition.withWork`    | —      | 6          |
| `BuildDefinition.withRules`   | —      | 5          |
| `BuildDefinition.withSandbox` | —      | 5          |
| `DockerOverrides.applyTo`     | —      | 8          |
| `WerkdockOverrides.applyTo`   | —      | 6          |

Every mutant of the new methods is killed, none is `INCONCLUSIVE`; each score equals its complexity.

The gate `maxMutationCrap` falls from 35 to 34: the highest score left is `CapacitySizing.cpu` at 33.17.

## Additional Changes

- Version 1.34.9 with its release note.
- `BuildExecutorUnitTest`, Scenario#108.05, waits for the executing build's process before it cancels the build: a build counts as running before its process is started, and a cancel in between left no process to see terminated (`List is empty`, seen once in this PR's local build).

## Follow-up PRs

- `CapacitySizing.cpu` (33.17) in its own PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #112
2026-10-04 18:51:22 +02:00

4.6 KiB

WARNING: This document describes only the change applied in this PR. It may already be outdated once the next PR is merged. Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.

The Problem

After PR#111 BuildDefinition.applyTo had the highest Mutation-CRAP score of Werkator: 34 at a complexity of 34. A score is never below the complexity, so no test could bring it under the limit of 15.

The method laid a build definition over its branch's settings in one copy of 21 fields, each with its own ?: fallback, the docker and werkdock blocks nested inside. What the definition says falls into three groups, but the method did not show them: what the build runs, the rules the instance runs it under, and the sandbox it runs in. Only one test checked a single field of it (Scenario#72.01); every other field was held only by tests of the config loader.

Non-Goals

  • No change in behaviour: which settings a definition can say, how they inherit, and which are pinned stay as they are.

The Scenarios

Feature: A build definition laid over its branch's settings

Scenario#112.01: A definition that says nothing runs the branch's settings unchanged

So that builds.<name> with only a trigger builds exactly what the branch describes.

  • Given a branch with a non-default value for every setting
  • When an empty definition, or one with empty docker and werkdock blocks, is applied
  • Then the result equals the branch's settings
Verified by

Scenario#112.02: Every setting a definition says replaces the branch's own

So that no key of builds.<name> is read and then silently dropped.

  • Given the same branch, and a definition saying every setting, docker and werkdock included, with other values
  • When the definition is applied
  • Then every setting is the definition's
Verified by

Scenario#112.03: A definition saying some settings of a block keeps the rest of that block

So that docker.image alone does not reset the branch's network, socket or environment.

  • Given the same branch, and a definition saying only the timeout, the docker image and the werkdock rootfs
  • When the definition is applied
  • Then those three are the definition's, everything else is the branch's
Verified by

The Solution

applyTo lays the definition over the branch in three blocks:

Block Settings
withWork buildCommand, cleanCommand, artifactDirs, stdoutLog, stderrLog
withRules requirePullRequest, statusContext, timeout, restartsInstance
withSandbox docker, werkdock

DockerOverrides.applyTo and WerkdockOverrides.applyTo lay each sandbox's overrides over the branch's sandbox of that kind. The overrides and their config class now stand side by side, where Scenario#72.03 already holds that every setting of the one exists in the other.

Measured with PIT as the nightly build runs it:

Method Before After
BuildDefinition.applyTo 34 no mutants
BuildDefinition.withWork — 6
BuildDefinition.withRules — 5
BuildDefinition.withSandbox — 5
DockerOverrides.applyTo — 8
WerkdockOverrides.applyTo — 6

Every mutant of the new methods is killed, none is INCONCLUSIVE; each score equals its complexity.

The gate maxMutationCrap falls from 35 to 34: the highest score left is CapacitySizing.cpu at 33.17.

Additional Changes

  • Version 1.34.9 with its release note.
  • BuildExecutorUnitTest, Scenario#108.05, waits for the executing build's process before it cancels the build: a build counts as running before its process is started, and a cancel in between left no process to see terminated (List is empty, seen once in this PR's local build).

Follow-up PRs

  • CapacitySizing.cpu (33.17) in its own PR.