{"record":{"id":"ae200507b967eafb","repo":"AlexsJones/llmfit","slug":"invalid-version-version-r","errorCode":null,"errorMessage":"Invalid version: {version!r}","messagePattern":"Invalid version: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"llmfit-python/hatch_build.py","lineNumber":74,"sourceCode":"\n    PLUGIN_NAME = \"llmfit version, license and readme\"\n\n    def update(self, metadata: dict) -> None:\n        \"\"\"Populate dynamic metadata from the repository.\n\n        ``version`` and ``license-expression`` come from ``Cargo.toml``,\n        ``readme`` from the repository root.\n\n        Version resolution order:\n\n        1. ``LLMFIT_VERSION`` environment variable (e.g. ``0.9.8``).\n        2. The ``version`` field in ``[workspace.package]`` from ``Cargo.toml``.\n        \"\"\"\n        with (Path(self.root).parent / \"Cargo.toml\").open(\"rb\") as f:\n            workspace_package: dict[str, str] = tomli.load(f)[\"workspace\"][\"package\"]\n        version: str = os.environ.get(\"LLMFIT_VERSION\") or workspace_package[\"version\"]\n        if not re.match(r\"^\\d+\\.\\d+\\.\\d+$\", version):\n            raise ValueError(f\"Invalid version: {version!r}\")\n        metadata[\"version\"] = version\n        metadata[\"license-expression\"] = workspace_package[\"license\"]\n\n        # The package README is the repository one. Hatchling refuses a\n        # `readme = \"../README.md\"` path (\"must be within the project\n        # directory\") but takes the contents verbatim, so read it here rather\n        # than duplicating or symlinking the file.\n        readme = Path(self.root).parent / \"README.md\"\n        metadata[\"readme\"] = {\n            \"content-type\": \"text/markdown\",\n            \"text\": readme.read_text(encoding=\"utf-8\"),\n        }\n\n\nclass LlmfitBinaryBuildHook(BuildHookInterface):\n    \"\"\"Hatchling build hook that injects the llmfit binary into each wheel.\"\"\"\n\n    PLUGIN_NAME = \"llmfit binary\"","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/AlexsJones/llmfit/blob/a9ac7ed91c1729cd93944bc1338e8313daaba8fa/llmfit-python/hatch_build.py#L56-L92","documentation":"LlmfitMetadataHook.update() resolves the Python package version: the LLMFIT_VERSION environment variable wins, otherwise [workspace.package].version from the repo-root Cargo.toml. It must match ^\\d+\\.\\d+\\.\\d+$ exactly (plain MAJOR.MINOR.PATCH); anything else aborts wheel metadata resolution with this ValueError, which fails uv build, uv sync, and pip builds of llmfit-python.","triggerScenarios":"Exporting LLMFIT_VERSION=0.9.8-rc.1 or 0.9.8+dev before building the wheel; bumping Cargo.toml's workspace version to a prerelease form like 0.10.0-rc.1; an LLMFIT_VERSION with a leading 'v' or trailing whitespace/newline from a CI step; an empty LLMFIT_VERSION string is falsy and falls through, but a malformed non-empty one always trips this.","commonSituations":"CI pipelines deriving LLMFIT_VERSION from a git tag (v1.2.3 keeps the 'v', 1.2.3-rc.1 keeps the suffix); release automation setting a pre-release identifier that Python allows but this strict regex forbids; a stale env var left in a shell from an earlier experiment.","solutions":["Echo $LLMFIT_VERSION and strip anything beyond MAJOR.MINOR.PATCH (drop 'v', suffixes, whitespace) or unset it: `unset LLMFIT_VERSION`.","If the version comes from Cargo.toml, set [workspace.package] version to a plain semver triplet and keep release identifiers elsewhere.","In CI, normalize before export: LLMFIT_VERSION=$(git describe --tags | sed -E 's/^v?([0-9]+\\.[0-9]+\\.[0-9]+).*$/\\1/').","Verify with `python -c \"import re,sys; print(bool(re.match(r'^\\d+\\.\\d+\\.\\d+$', sys.argv[1])))\" \"$LLMFIT_VERSION\"` before invoking the build."],"exampleFix":"# before\nexport LLMFIT_VERSION=\"v0.9.8-rc.1\"\nuv build\n\n# after\nexport LLMFIT_VERSION=\"0.9.8\"\nuv build","handlingStrategy":"validation","validationCode":"import os, re\n\nversion = os.environ.get('LLMFIT_VERSION') or read_cargo_version()\nif not re.match(r'^\\d+\\.\\d+\\.\\d+$', version):\n    version = re.search(r'\\d+\\.\\d+\\.\\d+', version).group(0)  # normalize, or fail loudly\nassert re.match(r'^\\d+\\.\\d+\\.\\d+$', version), f'bad version {version!r}'","typeGuard":null,"tryCatchPattern":"try:\n    subprocess.run(['uv', 'build'], check=True, env=env)\nexcept subprocess.CalledProcessError as e:\n    if 'Invalid version:' in (e.stderr or ''):\n        fix_and_retry_with_normalized_LLMFIT_VERSION()  # strip 'v', suffixes, whitespace\n    raise","preventionTips":["Normalize version strings to plain MAJOR.MINOR.PATCH at the point they enter CI (git describe, release vars).","Keep [workspace.package].version in Cargo.toml a bare semver triplet; track pre-release state elsewhere.","Echo LLMFIT_VERSION in build logs before the build step so mismatches are visible.","Unset LLMFIT_VERSION in local shells after release experiments."],"tags":["python","packaging","version","build"],"backgroundTag":"invalid-version-string","analyzedSha":"a9ac7ed91c1729cd93944bc1338e8313daaba8fa","analyzedAt":"2026-08-16T19:19:11.438Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}