Feature: Publish the RenewCMS image to a registry so an upgrade is "docker compose pull" As a maintainer I want one task that builds and pushes a versioned multi-arch image So that site operators upgrade with "docker compose pull && docker compose up -d" and never build from source Background: Given there is no CI pipeline on purpose; publishing is a maintainer-run task, like "task release" And the registry is the Gitea registry of git.floriansylvain.fr and REGISTRY_IMAGE defaults to "git.floriansylvain.fr/fsylvain/renewcms" And the Dockerfile and docker-compose.yml from docker-deployment exist and are unchanged except where stated here And Taskfile.yml gains "docker:publish", with variables REGISTRY_IMAGE and VERSION (default: git describe --tags) And "docker:publish" refuses to run, before building anything, when REGISTRY_IMAGE is empty, when VERSION is not an exact tag "vX.Y.Z" or "vX.Y.Z-
", or when the work tree is dirty
    And the build is "docker buildx build --platform linux/amd64,linux/arm64 --build-arg VERSION="; the tag is baked in through the existing version ldflags path, so the image's "renewcms --version" prints the tag
    And tags pushed for vX.Y.Z-
: "vX.Y.Z-
" and "X.Y.Z-
" and the moving tag "" (for example "beta"); "latest" is NOT moved
    And tags pushed for vX.Y.Z: "vX.Y.Z", "X.Y.Z", "X.Y", and "latest"
    And the maintainer authenticates beforehand with "docker login git.floriansylvain.fr"; the task never reads, stores or echoes a password or token, and never passes credentials on a command line
    And no secret, .env, signing key or JWT_SECRET is copied into the image (already enforced by .dockerignore; checked again here)
    And no image signing or attestation is part of this slice; the Ed25519 key signs release binaries only
    And "task docker:publish -- --dry-run" builds for the host platform only with "--load", pushes nothing, needs no login and prints the tags it would push
    And docker-compose.yml uses "image: ${RENEWCMS_IMAGE:-git.floriansylvain.fr/fsylvain/renewcms:beta}" (the default tag is "beta" because "latest" does not exist until the first stable release; it is changed to "latest" then) and its "build:" block moves to docker-compose.build.yml, used as "docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build"
    And docs/docker.md, docs/deployment.md and README.md document pull-based upgrade, the build override, the tag scheme and the maintainer publish steps; no comments in any file
    And the real push is a manual item: it needs a registry account and network

  Scenario Outline: Tags for a version
    Given VERSION is ""
    When "task docker:publish -- --dry-run" runs
    Then it prints exactly these tags: 

    Examples:
      | version      | tags                                  |
      | v2.9.0       | v2.9.0, 2.9.0, 2.9, latest            |
      | v2.9.0-beta  | v2.9.0-beta, 2.9.0-beta, beta         |
      | v2.9.0-rc.1  | v2.9.0-rc.1, 2.9.0-rc.1, rc           |

  Scenario Outline: Refusals before any build
    Given 
    When "task docker:publish" runs
    Then it exits non-zero with a message naming the problem and docker is not invoked

    Examples:
      | condition                             |
      | REGISTRY_IMAGE is empty               |
      | VERSION is "dev"                      |
      | VERSION is "2.9.0" (no leading v)     |
      | the work tree has uncommitted changes |

  Scenario: Dry run builds and loads a working image
    When "task docker:publish -- --dry-run" runs with VERSION "v2.9.0-beta"
    Then an image is loaded locally for the host platform and no registry is contacted
    And running it prints "v2.9.0-beta" for --version
    And it serves GET /healthz as ok with the env from docker.env.example
    And it runs as the non-root user with the data volume as in docker-deployment

  Scenario: No secrets in the image
    Given the dry-run image
    Then "docker history" and a file listing show no .env, no release.key, no JWT_SECRET value and no SMTP password

  Scenario: Compose pulls by default
    Given docker-compose.yml with RENEWCMS_IMAGE unset
    Then "docker compose config" shows the image "git.floriansylvain.fr/fsylvain/renewcms:beta" and no build section
    And with RENEWCMS_IMAGE set to a tag, that tag is used
    And with docker-compose.build.yml added, the build section is present and the image builds from the local Dockerfile

  Scenario: Upgrade is a pull
    Given a running stack on tag "v2.8.0-beta" with data in the volume
    When the operator runs "docker compose pull && docker compose up -d" after a newer tag is pushed
    Then the container is recreated on the new image and the articles, uploads and database in the volume are intact
    And GET /v1/settings reports the new version

  Scenario: Docs say the same thing
    Then docs/docker.md, docs/deployment.md and README.md each show the pull upgrade, the tag scheme and the maintainer flow, and none claims CI exists