import { Callout } from "zudoku/ui/Callout";

# Build versioning

Every CI run on a release line stamps a version into `app.json` and claims a `build/<version>` branch
for it. This page explains how the four version parts are decided, why the **build number** no longer
restarts after a version bump, and what a release pipeline reproduces from it.

The versioning work is done by the [Stamp Build Version](../devops-extension/build-tasks#stamp-build-version)
task (`Invoke-BcBuildVersionStamp`), driven by the shared CI and CD templates in
`businessdev.BC.Pipeline` (`yaml/v2/ci.yml`, `yaml/v2/cd.yml`).

---

## How the version is built

The version is expressed as a token schema. The CI uses `major.minor.increment.0`:

| Part | Where it comes from |
| --- | --- |
| Major | `app.json` (unchanged) |
| Minor | `app.json` (unchanged) |
| Build | the **build number** - seeded from the highest `build/*` branch, then incremented |
| Revision | `0` in CI; the pipeline build id in a CD release |

A version is only stamped - and only then is a `build/<version>` branch claimed and a package
published - on the release lines `dev` and `hotfix/*`. Every other run (`feature/*`, PR validation,
schedule) builds and tests without a version, without a build branch and without a package.

The `build/<version>` branch is the carrier: it is what a later run reads the already-claimed build
number back from, and it is what makes two parallel builds compute different versions.

---

## The build number runs across version bumps

Seeding the counter from the highest `build/<Major>.<Minor>.*` branch of the **current** `Major.Minor`
line has a consequence that only shows up when the version is raised: the new line has no build branch
yet, so the counter starts at 1 again.

```text
app.json 18.3 -> 18.4   last build branch 18.3.539.*   first build on the new line 18.4.1.0
```

While `Major.Minor` moves with every release that is merely untidy. It becomes a real problem when
`Major.Minor` stays put: the freshly built version then lands **below** the version customers already
have installed, and the app can no longer be upgraded.

```text
shipped to customers   18.12.239
CI keeps building      18.12.8.0     -> lower, so no upgrade path
```

`continuousBuildNumber` fixes this. With it enabled the build number is derived from the highest build
branch of the **whole repository**, across all lines, while Major and Minor still come from `app.json`:

```text
app.json 18.3 -> 18.4   highest build branch in the repo 18.3.539.*   ->  18.4.540.0
```

| Setting | Value | Effect |
| --- | --- | --- |
| `continuousBuildNumber` (CI template parameter) | `true` (default) | Build number runs on across all `build/*` branches of the repository |
| `continuousBuildNumber` | `false` | Old behaviour: the counter restarts per `<Major>.<Minor>` line |
| `-ContinuousBuildNumber` (`Invoke-BcBuildVersionStamp`) | switch | Same option at cmdlet level, for a local or hand-written pipeline |

A product that wants its build number to restart with every release line sets it in its own root
`ci.yml`:

```yaml
extends:
  template: yaml/v2/ci.yml@pipeline
  parameters:
    projectFolder: ''
    continuousBuildNumber: false
```

<Callout type="note" title="Requires StampBuildVersion 0.2 / module 2.59.0">
The CI template passes the `continuousBuildNumber` input to `StampBuildVersion@0`, which needs task
version `0.2` of the ALbuild Azure DevOps extension and module `2.59.0` or later.
</Callout>

<Callout type="caution" title="One-time seeding after the migration from the old pipeline">
The counter can only run on as high as a build branch already says it is. A repository whose earlier
versions were produced **without** build branches therefore needs a single `build/<version>` branch at
the height it had already reached, otherwise the first v2 build starts under the shipped version.
</Callout>

---

## What the CD does **not** change

The release pipeline is deliberately untouched by this. It takes the version the CI assigned from the
build branch of its **own release line** - schema `latest.latest.latest.build-id`, which reads the
highest `build/<Major>.<Minor>.*` branch and sets the fourth part to the build id:

```text
CI claimed 18.4.540.*    release run -> 18.4.540.<build-id>
```

That scoping is intentional: a release must reproduce the exact version its CI validated, not the
tallest number somewhere else in the repository. The `latest` token enables
`seedFromBuildBranch` automatically, and the read is read-only on the remote - a release never claims
a build branch.

| Pipeline | Build number scope | Reason |
| --- | --- | --- |
| CI (`StampBuildVersion` with `continuousBuildNumber`) | highest build branch of the repository | the number only has to keep rising |
| CD (`seedFromBuildBranch`) | highest build branch of its own `<Major>.<Minor>` line | the release must match what CI tested |

---

## Related

- [Stamp Build Version task inputs](../devops-extension/build-tasks#stamp-build-version) - schema tokens, `seedFromBuildBranch`, output variables.
- [Code coverage & test quality](code-coverage) - the coverage granularity the CI captures per test codeunit.
- [Invoke-BcBuildVersionStamp](../powershell-module/core#invoke-bcbuildversionstamp) - the cmdlet behind the task.
