Repository navigation
[Bug]: specify extension update crashes with KeyError when a catalog entry has no version, and stops checking the other extensions #5082
Description
Bug Description
discover_updates reads ext_info["version"] directly (src/specify_cli/extensions/_command_update_discovery.py:101). The only error it handles is InvalidVersion. Catalog entries are not validated field by field: _get_merged_extensions says on purpose that "one malformed entry shouldn't poison an otherwise valid catalog" (src/specify_cli/extensions/__init__.py:4815-4822). An installed extension whose catalog entry has no version therefore makes specify extension update exit with a raw KeyError, and the remaining installed extensions are never checked.
Steps to Reproduce
Save this as repro.py and run uv run python -I repro.py from the repository root. It pre-seeds the catalog cache, so no network request is made.
import hashlib, json, os, tempfile, traceback
from datetime import datetime, timezone
from pathlib import Path
from typer.testing import CliRunner
from specify_cli import app
root = Path(tempfile.mkdtemp())
proj = root / "project"
(proj / ".specify").mkdir(parents=True)
(proj / ".claude/skills").mkdir(parents=True)
os.chdir(proj)
for eid in ("aaa", "bbb"):
d = root / eid
(d / "commands").mkdir(parents=True)
(d / "commands/hello.md").write_text("---\ndescription: x\n---\n\nHello\n")
(d / "extension.yml").write_text(f"""schema_version: "1.0"
extension: {{id: {eid}, name: {eid}, version: 1.0.0, description: d}}
requires: {{speckit_version: ">=0.1.0"}}
provides:
commands:
- {{name: speckit.{eid}.hello, file: commands/hello.md, description: x}}
""")
CliRunner().invoke(app, ["extension", "add", "--dev", str(d)])
# Pre-seed a fresh catalog cache so no network request is made.
url = "https://catalog.example.invalid/catalog.json"
os.environ["SPECKIT_CATALOG_URL"] = url
cache = proj / ".specify/extensions/.cache"
cache.mkdir(parents=True, exist_ok=True)
h = hashlib.sha256(url.encode()).hexdigest()[:16]
(cache / f"catalog-{h}.json").write_text(json.dumps({"schema_version": "1.0", "extensions": {
"aaa": {"name": "aaa", "download_url": "https://catalog.example.invalid/aaa.zip"}, # no "version"
"bbb": {"name": "bbb", "version": "1.0.0"},
}}))
(cache / f"catalog-{h}-metadata.json").write_text(json.dumps(
{"cached_at": datetime.now(timezone.utc).isoformat(), "catalog_url": url}))
r = CliRunner().invoke(app, ["extension", "update"])
print("exit code:", r.exit_code)
print(r.output)
if r.exception and not isinstance(r.exception, SystemExit):
print("".join(traceback.format_exception(r.exception)[-3:]))Expected Behavior
aaa is skipped with a warning and bbb is reported as up to date. That is how the same function treats an invalid catalog version (_command_update_discovery.py:102-107, "Invalid catalog version ... (skipping)"). It also fits the per-entry tolerance stated at __init__.py:4815-4822.
Actual Behavior
exit code: 1
🔄 Checking for updates...
Warning: Using non-default extension catalog. Only use catalogs from sources you trust.
The command exits 1 with the traceback below. bbb is never checked.
Specify CLI Version
1.2.4.dev0 (main at 27d14e2)
AI Agent
Not applicable
Operating System
Linux 6.18 (Ubuntu container)
Python Version
Python 3.12.3
Error Logs
File "<checkout>/src/specify_cli/extensions/_command_update_transaction.py", line 42, in run_update_command
updates_available, blocked_updates, has_installed = discover_updates(
^^^^^^^^^^^^^^^^^
File "<checkout>/src/specify_cli/extensions/_command_update_discovery.py", line 101, in discover_updates
catalog_version = pkg_version.Version(ext_info["version"])
~~~~~~~~^^^^^^^^^^^
KeyError: 'version'
Additional Context
Using ext_info.get("version") and treating a missing value like an invalid one would fix this.
Also reproduced on macOS (Darwin 25.6, arm64) with Python 3.14.4 and uv run python -I repro.py against 27d14e2; the output matches the above.
AI Disclosure
Found and reproduced with Claude Code (model: Claude Opus 4.8), run autonomously under human supervision. The human reporter re-ran every reproduction above before filing.
Activity