# ADR: The version comes from the changelog

Status: accepted, 2026-09-03, shipped as 1.0.0.41. This supersedes the version
formula in ADR: Version in the footer, ADR: The deploy pipeline and
ADR: The changelog. Those records keep their reasoning; the number is computed
differently now.

## In plain words

This page makes the top line of the changelog the site's version number. The deploy reads the number from that line, which means the footer cannot disagree with the changelog. A deploy whose line is missing or not above the line below it fails instead of shipping.

What that is worth: a developer never has to remember a number or an offset, and the organization's live footer always matches the file that documents it.

## Context

The displayed version was `1.0.0.(11 + deploy run number)`. The changelog's top
line named the number the next deploy would produce, written by hand, one past
whatever the footer showed.

ADR: The changelog already knew the weakness and wrote it down:

> The deploy's changelog check would not have caught it. It asks whether a line
> exists for the version being shipped, which was true; it cannot know that a
> line further up describes a version that never will. The cheap guard is
> procedural.

The procedural guard lasted about a day.

On 2026-09-03 the style job's first CI run went red. Deploy fires on
`workflow_run` and is gated by a conditional, so the run started, skipped, and
consumed run number 28. The fix went out on the next green CI as run 29, which
made it 1.0.0.40, while the changelog line written with that commit said
1.0.0.39. The live footer and the file that documents the live footer
disagreed, in a project whose stated selling point is that they cannot.

Nothing was broken. A visitor comparing the footer to the changelog would have
found a portfolio site contradicting itself, which is worse than broken.

## Decision

**The changelog's top line is the version.** The deploy reads it, and there is
no offset and no run counter in it any more.

```
- **1.0.0.41** (2026-09-03): one sentence.
   ^^^^^^^^^^ this is what the footer shows and what tags the image
```

Two things follow immediately, and both are the point:

- A skipped or repeated run number cannot move the version, because the run
  number no longer appears in it.
- A ship that forgets its changelog line fails the deploy instead of shipping
  a number nothing describes. The check that used to warn now decides.

**The validation is a hard failure, in two parts.** No parseable line at the
top stops the deploy. A top line that is not strictly above the line below it
stops the deploy, which is the "forgot to bump" case: the second line is by
definition the last thing that shipped.

**The API test keeps the same file honest from the other side.** It already
asserted the shape, the descending order, no repeats and no em dash. It now
also asserts the top line is above the second, which is the same rule the
workflow enforces, so a mistake is caught in CI a minute before it would be
caught by a failed deploy.

**Numbers may still skip, and 1.0.0.39 is the first one that did.** That gap is
kept rather than renumbered. A version that never ran is not a version, and
rewriting history to hide a red build is the opposite of what this file is for.

## In the code

The version step (`.github/workflows/deploy.yml`):

```yaml
# Pin to the exact commit the gate tested and pushed, not wherever main has moved since.
- uses: actions/checkout@v7
  with:
    ref: ${{ github.sha }}

- name: Compute version
  env:
    HEAD_SHA: ${{ github.sha }}
  run: |
    # The changelog's top line is the version (ADR-031). Not a run
    # counter: a counter ticks on runs that are gated off and never ship,
    # which is how 1.0.0.39 became a number the changelog named and
    # nothing ever displayed.
    # Four numbers since 1.0.1.0, compared as a version (sort -V), not as
    # the one counter it was while every version began 1.0.0.
    VERSIONS=$(sed -n 's/^- \*\*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)\*\*.*/\1/p' docs/CHANGELOG.md)
    N=$(echo "$VERSIONS" | sed -n 1p)
    PREV=$(echo "$VERSIONS" | sed -n 2p)
    # #region changelog-check
    # Both of these used to be a warning nobody reads. They stop the
    # deploy now, because the file they check is where the version comes
    # from: shipping past either one means shipping a number with no
    # sentence attached to it (ADR-031).
    if [ -z "$N" ]; then
      echo "::error file=docs/CHANGELOG.md::No version line at the top of the changelog, so there is no version to ship"
      exit 1
    fi
    if [ -n "$PREV" ] && { [ "$N" = "$PREV" ] || [ "$(printf '%s\n%s\n' "$PREV" "$N" | sort -V | tail -1)" != "$N" ]; }; then
      echo "::error file=docs/CHANGELOG.md::The top line is $N and $PREV is already below it, so this ship has no changelog line of its own"
      exit 1
    fi
    # #endregion changelog-check
    {
      echo "N=$N"
      echo "APP_VERSION=$N"
      echo "APP_COMMIT=${HEAD_SHA:0:7}"
      echo "IMAGE=$ACR/theyard:v$N"
    } >> "$GITHUB_ENV"
    echo "Deploying $N from ${HEAD_SHA:0:7} as $ACR/theyard:v$N"
```
*Live from [`.github/workflows/deploy.yml`](https://github.com/SteveStout/TheYard/blob/1d6b50b/.github/workflows/deploy.yml#L75-L113), region `compute-version`, read from this build at 1d6b50b.*

The refusal (`.github/workflows/deploy.yml`):

```yaml
# Both of these used to be a warning nobody reads. They stop the
# deploy now, because the file they check is where the version comes
# from: shipping past either one means shipping a number with no
# sentence attached to it (ADR-031).
if [ -z "$N" ]; then
  echo "::error file=docs/CHANGELOG.md::No version line at the top of the changelog, so there is no version to ship"
  exit 1
fi
if [ -n "$PREV" ] && { [ "$N" = "$PREV" ] || [ "$(printf '%s\n%s\n' "$PREV" "$N" | sort -V | tail -1)" != "$N" ]; }; then
  echo "::error file=docs/CHANGELOG.md::The top line is $N and $PREV is already below it, so this ship has no changelog line of its own"
  exit 1
fi
```
*Live from [`.github/workflows/deploy.yml`](https://github.com/SteveStout/TheYard/blob/1d6b50b/.github/workflows/deploy.yml#L94-L105), region `changelog-check`, read from this build at 1d6b50b.*

The same rule from the other side (`api/TheYard.Tests/ChangelogTests.cs`):

```csharp
/// <summary>
/// The same rule the deploy enforces (ADR: The version comes from the
/// changelog). The top line is the version about to ship, so it has to sit
/// above the one below it. A commit that forgets its own line leaves the
/// last shipped version on top, and this notices a minute before the
/// deploy would.
/// </summary>
[Fact]
public async Task The_top_line_is_above_the_one_below_it()
{
    string markdown = await _client.GetStringAsync("/api/docs/changelog");

    var versions = markdown.Split('\n')
        .Select(line => EntryLine.Match(line.TrimEnd('\r')))
        .Where(match => match.Success)
        .Select(match => Version.Parse(match.Groups[1].Value))
        .ToList();

    Assert.True(versions.Count >= 2, "two versions are needed before this rule means anything");
    Assert.True(
        versions[0] > versions[1],
        $"the top line is {versions[0]} and the line below it is {versions[1]}, "
        + "so this ship has no changelog line of its own and the deploy will refuse it");
}
```
*Live from [`api/TheYard.Tests/ChangelogTests.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Tests/ChangelogTests.cs#L65-L88), region `version-order`, read from this build at 1d6b50b.*

## Consequences

- The footer and the changelog cannot disagree. Not "should not": the string in
  the footer is read from the file that documents it, so a disagreement would
  require the deploy to have used a different file.
- Writing the changelog line stops being a convention and becomes a step of the
  build. Skipping it is a red deploy, not a warning nobody reads.
- The image tag follows the same number, so `v41` in the registry is the image
  that says 1.0.0.41, and no run of the workflow can produce a tag that already
  exists without also failing its version check.
- The `OFFSET: 11` environment variable is gone. It was the last thing in the
  pipeline that had to be remembered rather than derived.
- One thing this deliberately does not do is derive the version from a git tag.
  That would be the right answer for a library with releases. Here the changelog
  is already mandatory, already tested, and already served to the reader, so it
  is the artefact with the fewest ways to be wrong.

## Where it sits

No ring: this is a deployment decision about where the version number comes from, so the onion and SOLID do not apply to it.

## Files

- [`.github/workflows/deploy.yml`](https://github.com/SteveStout/TheYard/blob/main/.github/workflows/deploy.yml): the version step and the refusal.
- [`api/TheYard.Tests/ChangelogTests.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Tests/ChangelogTests.cs): the same rule, enforced a minute earlier.
- [`docs/CHANGELOG.md`](https://github.com/SteveStout/TheYard/blob/main/docs/CHANGELOG.md): now an input to the build.
- [`docs/decisions/ADR-012-changelog.md`](https://github.com/SteveStout/TheYard/blob/main/docs/decisions/ADR-012-changelog.md): the record that predicted this failure and chose the procedural guard.
- [`docs/decisions/ADR-005-version-footer.md`](https://github.com/SteveStout/TheYard/blob/main/docs/decisions/ADR-005-version-footer.md), [`docs/decisions/ADR-009-deploy-pipeline.md`](https://github.com/SteveStout/TheYard/blob/main/docs/decisions/ADR-009-deploy-pipeline.md): where the old formula was written down.

## Addendum, 2026-09-22 (1.0.1.0): four numbers, compared as a version

Steve set the first release at 1.0.1.0, the first version he considered polished. Until then every version began 1.0.0, so the deploy read only the last number and compared it with `-le`. From 1.0.1.0 the deploy reads all four numbers off the changelog's top line and compares it with the line below using `sort -V`, so 1.0.1.0 ships over 1.0.0.187, and a repeated or lower version is refused as before. `APP_VERSION` is the version itself, and the image is tagged `v<version>` (`v1.0.1.0`), which the second site's deploy waits for by the same name. `ChangelogTests` parses each line with `System.Version`, so the test and the workflow still enforce one rule. The Consequences above describe the tags as they were through 1.0.0.187 (`v41` for 1.0.0.41) and are kept as the record.
