Probe the RPM filename nfpm actually publishes #175
@@ -8,4 +8,9 @@ dev = [
|
||||
"pytest>=8",
|
||||
"jsonschema>=4",
|
||||
"pyyaml>=6",
|
||||
# tools/build's own script dependencies, so tests can import it
|
||||
"typer",
|
||||
"requests",
|
||||
"hvac",
|
||||
"cerberus",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Tests for the RPM filename tools/build reconstructs when probing artifactapi.
|
||||
|
||||
The expected values below were captured from the real nfpm; they are the names
|
||||
the publish step actually uploads, so the existence probe has to ask for
|
||||
exactly these or it 404s and rebuilds forever.
|
||||
"""
|
||||
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).parent.parent
|
||||
|
||||
_loader = importlib.machinery.SourceFileLoader(
|
||||
"rpmbuilder_build", str(REPO_ROOT / "tools" / "build")
|
||||
)
|
||||
_spec = importlib.util.spec_from_loader(_loader.name, _loader)
|
||||
build = importlib.util.module_from_spec(_spec)
|
||||
_loader.exec_module(build)
|
||||
|
||||
|
||||
# nfpm output for `version: <input>` with release 1, arch amd64
|
||||
NFPM_VERSIONS = [
|
||||
("2025.08.03", "2025.8.3"),
|
||||
("2025.07.13", "2025.7.13"),
|
||||
("1.05.0", "1.5.0"),
|
||||
("10.02.01", "10.2.1"),
|
||||
("0.0.09", "0.0.9"),
|
||||
("0.6.1", "0.6.1"),
|
||||
("1.2.3", "1.2.3"),
|
||||
("1.05", "1.5.0"),
|
||||
("08", "8.0.0"),
|
||||
("1.0.0-rc1", "1.0.0~rc1"),
|
||||
("1.05.0-rc1", "1.5.0~rc1"),
|
||||
("1.0.0-rc.1", "1.0.0~rc.1"),
|
||||
# not semver: nfpm leaves these verbatim, so we must too
|
||||
("1.02.3.4", "1.02.3.4"),
|
||||
("2025.08.03.01", "2025.08.03.01"),
|
||||
("1.2.3.4", "1.2.3.4"),
|
||||
("1.0.0-rc.01", "1.0.0-rc.01"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version,expected", NFPM_VERSIONS)
|
||||
def test_nfpm_rpm_version(version, expected):
|
||||
assert build.nfpm_rpm_version(version) == expected
|
||||
|
||||
|
||||
def test_rpm_file_name_matches_nfpm_output():
|
||||
assert (
|
||||
build.rpm_file_name("nzbget_exporter", "2025.08.03", "1.el9", "amd64")
|
||||
== "nzbget_exporter-2025.8.3-1.el9.x86_64.rpm"
|
||||
)
|
||||
|
||||
|
||||
def test_check_package_exists_probes_published_filename(monkeypatch):
|
||||
"""A zero-padded version must not probe a filename nfpm can never produce."""
|
||||
probed = []
|
||||
|
||||
class _Session:
|
||||
def get(self, url, timeout=None):
|
||||
probed.append(url)
|
||||
return type("R", (), {"status_code": 404})()
|
||||
|
||||
monkeypatch.setattr(build, "_artifactapi_session", _Session())
|
||||
|
||||
assert not build.check_package_exists(
|
||||
"nzbget_exporter", "2025.08.03", "1.el9", "almalinux/el9", "amd64"
|
||||
)
|
||||
expected = (
|
||||
"https://artifactapi.k8s.syd1.au.unkin.net/api/v2/remotes/rpm-vendor-el9"
|
||||
"/files/Packages/nzbget_exporter-2025.8.3-1.el9.x86_64.rpm"
|
||||
)
|
||||
assert probed == [expected]
|
||||
+33
-39
@@ -517,37 +517,38 @@ def get_github_token() -> str:
|
||||
|
||||
# ==================== GITEA API FUNCTIONS ====================
|
||||
|
||||
def normalize_version(version: str) -> str:
|
||||
def nfpm_rpm_version(version: str) -> str:
|
||||
"""
|
||||
Normalize version string by removing leading zeros from numeric components.
|
||||
Gitea automatically does this normalization.
|
||||
Render a metadata version the way nfpm renders it into an RPM filename.
|
||||
|
||||
nfpm re-renders any semver-parseable version: leading zeros are dropped,
|
||||
missing components padded to three, and the prerelease joined with '~'.
|
||||
Anything semver cannot parse (four or more components, non-numeric
|
||||
components, a numeric prerelease identifier with a leading zero) is used
|
||||
verbatim.
|
||||
|
||||
Examples:
|
||||
"2025.08.03" -> "2025.8.3"
|
||||
"1.05.0" -> "1.5.0"
|
||||
"0.6.1" -> "0.6.1" (no change needed)
|
||||
|
||||
Args:
|
||||
version: Original version string
|
||||
|
||||
Returns:
|
||||
Normalized version string
|
||||
"1.05" -> "1.5.0"
|
||||
"1.2.3-rc1" -> "1.2.3~rc1"
|
||||
"1.02.3.4" -> "1.02.3.4" (not semver, left alone)
|
||||
"""
|
||||
import re
|
||||
core, sep, prerelease = version.partition('-')
|
||||
if sep and not prerelease:
|
||||
return version
|
||||
|
||||
# Split by common separators and normalize each numeric part
|
||||
parts = re.split(r'([.\-_])', version)
|
||||
normalized_parts = []
|
||||
components = core.split('.')
|
||||
if len(components) > 3 or not all(c.isdigit() for c in components):
|
||||
return version
|
||||
|
||||
for part in parts:
|
||||
# If this part is purely numeric and has leading zeros, remove them
|
||||
if part.isdigit() and len(part) > 1 and part.startswith('0'):
|
||||
# Remove leading zeros but keep at least one digit
|
||||
normalized_parts.append(str(int(part)))
|
||||
else:
|
||||
normalized_parts.append(part)
|
||||
# semver rejects numeric prerelease identifiers with a leading zero
|
||||
if any(i.isdigit() and len(i) > 1 and i.startswith('0')
|
||||
for i in prerelease.split('.')):
|
||||
return version
|
||||
|
||||
return ''.join(normalized_parts)
|
||||
components += ['0'] * (3 - len(components))
|
||||
rendered = '.'.join(str(int(c)) for c in components)
|
||||
return f"{rendered}~{prerelease}" if prerelease else rendered
|
||||
|
||||
|
||||
def get_rpm_dist_tag(distro: str) -> str:
|
||||
@@ -598,6 +599,14 @@ def get_rpm_arch(arch: str) -> str:
|
||||
return {'amd64': 'x86_64', 'arm64': 'aarch64'}.get(arch, arch)
|
||||
|
||||
|
||||
def rpm_file_name(package_name: str, version: str, release: str, arch: str) -> str:
|
||||
"""Filename nfpm produces for this package, and therefore the published name."""
|
||||
return (
|
||||
f"{package_name}-{nfpm_rpm_version(version)}-{release}"
|
||||
f".{get_rpm_arch(arch)}.rpm"
|
||||
)
|
||||
|
||||
|
||||
def check_package_exists(
|
||||
package_name: str,
|
||||
version: str,
|
||||
@@ -627,7 +636,7 @@ def check_package_exists(
|
||||
|
||||
base_url = os.getenv('ARTIFACTAPI_URL', 'https://artifactapi.k8s.syd1.au.unkin.net')
|
||||
repo = get_vendor_repo(distro)
|
||||
rpm_file = f"{package_name}-{version}-{release}.{get_rpm_arch(arch)}.rpm"
|
||||
rpm_file = rpm_file_name(package_name, version, release, arch)
|
||||
url = f"{base_url}/api/v2/remotes/{repo}/files/Packages/{rpm_file}"
|
||||
|
||||
try:
|
||||
@@ -651,21 +660,6 @@ def check_package_exists(
|
||||
return False
|
||||
|
||||
|
||||
def get_package_full_name(package_name: str, version: str, release: str) -> str:
|
||||
"""
|
||||
Generate the full package name as used in the registry.
|
||||
|
||||
Args:
|
||||
package_name: Package name
|
||||
version: Version string
|
||||
release: Release number
|
||||
|
||||
Returns:
|
||||
Full package name string
|
||||
"""
|
||||
return f"{package_name}-{version}-{release}"
|
||||
|
||||
|
||||
# ==================== DOCKER FUNCTIONS ====================
|
||||
|
||||
def check_docker_available() -> bool:
|
||||
|
||||
Reference in New Issue
Block a user