Skip to content

[Bug]: specify extension update crashes with KeyError when a catalog entry has no version, and stops checking the other extensions #5082

Open

Description

@hnipps

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

  1. added
    triage-must-haveVerdict: high-value, important work for Spec Kit — do first
    on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    triage-must-haveVerdict: high-value, important work for Spec Kit — do first

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions