Ctrl K
ComunidadGuías…

tests/test_release_version.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""The release's tag is the only place a version is written.

Chart.yaml's `appVersion` picks the image every Helm install runs, and it was
kept by hand. It said 0.64.0 for every release from 0.64.0 to 0.67.1, so an
install of any of them ran 0.64.0, without the fixes four advisories told
people to upgrade for. `pyproject.toml` said 0.64.0 alongside it, and the test
that compared the two passed, because neither had moved.

So nothing in the tree carries a release number any more. The release packages
the chart with the tag's version, and the project reads its own from the tag.
"""

from __future__ import annotations

import os
import re
import shutil
import subprocess
import tomllib
from pathlib import Path

import pytest
import yaml

ROOT = Path(__file__).resolve().parent.parent
CHART = ROOT / "deploy" / "helm" / "stocktake"
RELEASE = ROOT / ".github" / "workflows" / "release.yml"
HELM = shutil.which("helm")

needs_helm = pytest.mark.skipif(
    HELM is None, reason="helm is not installed here; CI's runners have it")


def test_the_project_takes_its_version_from_the_tag():
    """A number here is one more to forget at a release; this one already was."""
    pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))
    project = pyproject["project"]

    assert "version" not in project, (
        f"pyproject.toml says version {project['version']!r}; the tag is the version")
    assert "version" in project.get("dynamic", [])
    assert pyproject["tool"]["hatch"]["version"]["source"] == "vcs"


def test_what_builds_the_project_is_pinned():
    """uv.lock holds what the project installs, not what builds it, so
    hatchling and hatch-vcs resolved to whatever was newest in every job that
    installs the project, the one that tests a release among them."""
    pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))
    pinned = {pin.split("==")[0].lower()
              for pin in pyproject["tool"]["uv"].get("build-constraint-dependencies", [])
              if re.fullmatch(r"[A-Za-z0-9_.-]+==[0-9][0-9A-Za-z.]*", pin)}
    required = {re.match(r"[A-Za-z0-9_.-]+", requirement).group(0).lower()
                for requirement in pyproject["build-system"]["requires"]}

    assert required and required <= pinned, sorted(required - pinned)


def test_the_chart_here_names_no_release():
    """The chart in the tree is the source the release packages, not a release."""
    chart = yaml.safe_load((CHART / "Chart.yaml").read_text(encoding="utf-8"))

    assert "appVersion" not in chart, (
        f"Chart.yaml names appVersion {chart['appVersion']!r}; the release sets it")


def _helm_template(*args: str) -> subprocess.CompletedProcess:
    return subprocess.run([HELM, "template", "stocktake", *args],
                          capture_output=True, text=True)


def _deployment(rendered: str) -> dict:
    return next(doc for doc in yaml.safe_load_all(rendered)
                if doc and doc.get("kind") == "Deployment")


@needs_helm
def test_the_chart_here_refuses_to_guess_an_image():
    """Installed from a clone, it has no release to run, so it says where the
    published chart is rather than inventing one. A tag set by hand still wins,
    for anyone who means to run the chart from a clone."""
    refused = _helm_template(str(CHART))

    assert refused.returncode != 0, "the chart rendered an image it was not given"
    assert "oci://ghcr.io/jarrodruggiero/charts/stocktake" in refused.stderr

    chosen = _helm_template(str(CHART), "--set", "image.tag=0.67.1")
    assert chosen.returncode == 0, chosen.stderr
    container = _deployment(chosen.stdout)["spec"]["template"]["spec"]["containers"][0]
    assert container["image"] == "ghcr.io/jarrodruggiero/stocktake:0.67.1"


def _release_steps() -> list[dict]:
    document = yaml.safe_load(RELEASE.read_text(encoding="utf-8"))
    return [step for job in document["jobs"].values() for step in job.get("steps", [])]


def _release_step(step_id: str) -> dict | None:
    return next((s for s in _release_steps() if s.get("id") == step_id), None)


@needs_helm
def test_the_released_chart_runs_its_own_release(tmp_path):
    """The release's own step, with real Helm doing the packaging and only the
    registry calls stood in for: what it would push deploys the tag's image."""
    step = _release_step("chart")
    assert step is not None, "release.yml does not publish the Helm chart"
    bin_dir = tmp_path / "bin"
    bin_dir.mkdir()
    helm = bin_dir / "helm"
    helm.write_text("#!/bin/sh\n"
                    'case "$1" in push|registry) cat > /dev/null; exit 0;; esac\n'
                    f'exec "{HELM}" "$@"\n', encoding="utf-8")
    helm.chmod(0o755)

    result = subprocess.run(
        ["bash", "-e", "-c", step["run"]], cwd=ROOT, capture_output=True, text=True,
        stdin=subprocess.DEVNULL,
        env={**os.environ, "PATH": f"{bin_dir}:{os.environ['PATH']}",
             "RUNNER_TEMP": str(tmp_path), "VERSION": "1.2.3",
             "OWNER": "o", "ACTOR": "a", "TOKEN": "t"})
    assert result.returncode == 0, result.stderr

    rendered = _helm_template(str(tmp_path / "stocktake-1.2.3.tgz"))
    assert rendered.returncode == 0, rendered.stderr
    deployment = _deployment(rendered.stdout)
    container = deployment["spec"]["template"]["spec"]["containers"][0]
    assert container["image"] == "ghcr.io/jarrodruggiero/stocktake:1.2.3"
    assert deployment["metadata"]["labels"]["app.kubernetes.io/version"] == "1.2.3"


TAGS = ["v0.9.0", "v0.66.3", "v0.67.0", "v0.67.9", "v0.67.10", "v0.68.0-rc1"]


@pytest.mark.parametrize("tag, latest, minor", [
    ("v0.67.10", "true", "true"),       # newer than 0.9.0 and 0.67.9: versions, not text
    ("v0.67.9", "false", "false"),
    ("v0.66.3", "false", "true"),       # the newest 0.66, not the newest
    ("v0.68.0-rc1", "false", "false"),  # a prerelease moves neither
])
def test_latest_follows_the_newest_release_not_the_last_one_built(tmp_path, tag, latest, minor):
    """Building an older tag again by hand repointed `latest` and its `X.Y` at
    it, and a `:latest` install then refused to start against the database the
    newer release had migrated."""
    step = _release_step("newest")
    assert step is not None, "release.yml does not work out the newest release"
    bin_dir = tmp_path / "bin"
    bin_dir.mkdir()
    git = bin_dir / "git"
    git.write_text("#!/bin/sh\n" + "".join(f"printf '0000\\trefs/tags/{t}\\n'\n" for t in TAGS),
                   encoding="utf-8")
    git.chmod(0o755)
    output = tmp_path / "output"

    result = subprocess.run(
        ["bash", "-e", "-c", step["run"]], capture_output=True, text=True,
        stdin=subprocess.DEVNULL,
        env={**os.environ, "PATH": f"{bin_dir}:{os.environ['PATH']}", "TAG": tag,
             "GITHUB_OUTPUT": str(output)})

    assert result.returncode == 0, result.stderr
    said = dict(line.split("=", 1) for line in output.read_text(encoding="utf-8").split())
    assert said == {"latest": latest, "minor": minor}


def test_the_image_tags_and_the_release_ask_which_is_newest():
    by_action = {step["uses"].split("@")[0]: step.get("with", {})
                 for step in _release_steps() if "uses" in step}
    meta = by_action["docker/metadata-action"]
    minor = [line for line in meta["tags"].splitlines() if "{{major}}.{{minor}}" in line]

    assert "latest=${{ steps.newest.outputs.latest }}" in meta["flavor"].splitlines()
    assert minor and all("enable=${{ steps.newest.outputs.minor }}" in line for line in minor)
    assert (by_action["softprops/action-gh-release"]["make_latest"]
            == "${{ steps.newest.outputs.latest }}")