mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix(bundler): reject unsupported schema_version in _merge_config (align readers) (#3711)
bundle-catalogs.yml has two readers that are meant to agree: commands_impl/ catalog_config._read (bundle catalog list/add/remove) and models/catalog. _merge_config (the resolution path feeding bundle search/info/install via load_source_stack). _read rejects an unsupported MAJOR schema_version; _merge_config never checked it, so a file written by a newer/incompatible Spec Kit (e.g. schema_version '2.0') was silently parsed under v1 assumptions on the exact path where install_policy governs trust — the two readers disagreed. #3623 (non-list catalogs) and #3659 (top-level non-mapping) already aligned these two readers guard-by-guard; this is the last unaligned guard. Add the same forward-compatible major-version check to _merge_config. Promote CONFIG_SCHEMA_VERSION to models/catalog.py as the single source of truth and import it in catalog_config.py (was a local duplicate) so the two cannot drift. Absent schema_version stays valid (backward compatible); matching major stays valid. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,14 +14,13 @@ from .. import BundlerError
|
||||
from ..lib.yamlio import dump_yaml, ensure_within, load_yaml
|
||||
from ..models.catalog import (
|
||||
CONFIG_FILENAME,
|
||||
CONFIG_SCHEMA_VERSION,
|
||||
BUILTIN_DEFAULT_STACK,
|
||||
CatalogSource,
|
||||
InstallPolicy,
|
||||
Scope,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA_VERSION = "1.0"
|
||||
|
||||
_BUILTIN_IDS = {raw["id"] for raw in BUILTIN_DEFAULT_STACK}
|
||||
|
||||
# Windows absolute paths like ``C:\catalog.json`` parse with a single-letter
|
||||
|
||||
@@ -15,6 +15,11 @@ from .. import BundlerError
|
||||
from ..lib.yamlio import ensure_within, load_yaml
|
||||
|
||||
CONFIG_FILENAME = "bundle-catalogs.yml"
|
||||
# Supported bundle-catalogs.yml schema (major version). Both readers of the
|
||||
# file — this module's _merge_config and commands_impl/catalog_config._read —
|
||||
# reject an unsupported major version so a file written by a newer/incompatible
|
||||
# Spec Kit fails fast instead of being parsed under the wrong assumptions.
|
||||
CONFIG_SCHEMA_VERSION = "1.0"
|
||||
|
||||
|
||||
class InstallPolicy(str, Enum):
|
||||
@@ -267,6 +272,23 @@ def _merge_config(by_id: dict[str, CatalogSource], config_path: Path, scope: Sco
|
||||
f"Malformed catalog config at {config_path}: expected a mapping at "
|
||||
f"the top level, got {type(data).__name__}."
|
||||
)
|
||||
# Reject an unsupported major schema version, matching the sibling reader
|
||||
# commands_impl/catalog_config._read. Without this, a file written by a
|
||||
# newer/incompatible Spec Kit was silently parsed under v1 assumptions on
|
||||
# the resolution path (bundle search/install), while the other reader
|
||||
# rejected it — the two readers disagreed. An absent schema_version stays
|
||||
# valid (backward compatible with configs that omit it).
|
||||
schema_version = data.get("schema_version")
|
||||
if schema_version is not None and (
|
||||
str(schema_version).strip().split(".")[0]
|
||||
!= CONFIG_SCHEMA_VERSION.split(".")[0]
|
||||
):
|
||||
raise BundlerError(
|
||||
f"Unsupported catalog config schema version "
|
||||
f"'{str(schema_version).strip()}' at {config_path}; this Spec Kit "
|
||||
f"understands version {CONFIG_SCHEMA_VERSION}. The file may have been "
|
||||
"written by a newer version or is corrupt."
|
||||
)
|
||||
catalogs = data.get("catalogs")
|
||||
if catalogs is None:
|
||||
return
|
||||
|
||||
@@ -113,6 +113,44 @@ def test_absent_or_empty_catalogs_is_noop(tmp_path: Path, body: str):
|
||||
assert len(sources) > 0
|
||||
|
||||
|
||||
def test_load_source_stack_rejects_unknown_schema_version(tmp_path: Path):
|
||||
"""A bundle-catalogs.yml with an unsupported MAJOR schema_version must raise
|
||||
on the resolution path (load_source_stack -> _merge_config), matching the
|
||||
sibling reader commands_impl/catalog_config._read. Without this a file
|
||||
written by a newer/incompatible Spec Kit was silently parsed under v1
|
||||
assumptions on the install/search path, while the other reader rejected it."""
|
||||
make_project(tmp_path)
|
||||
config = {
|
||||
"schema_version": "2.0",
|
||||
"catalogs": [{"id": "corp", "url": "https://corp/catalog.json",
|
||||
"priority": 1, "install_policy": "install-allowed"}],
|
||||
}
|
||||
(tmp_path / ".specify" / "bundle-catalogs.yml").write_text(
|
||||
yaml.safe_dump(config), encoding="utf-8"
|
||||
)
|
||||
with pytest.raises(BundlerError, match="Unsupported catalog config schema version"):
|
||||
load_source_stack(tmp_path)
|
||||
|
||||
|
||||
def test_load_source_stack_accepts_matching_or_absent_schema_version(tmp_path: Path):
|
||||
"""A matching major version (1.x) and an absent schema_version both stay
|
||||
valid — the guard rejects only a different major, so existing configs that
|
||||
omit the key are unaffected."""
|
||||
make_project(tmp_path)
|
||||
cfg = tmp_path / ".specify" / "bundle-catalogs.yml"
|
||||
cfg.write_text(yaml.safe_dump({
|
||||
"schema_version": "1.5", # same major as CONFIG_SCHEMA_VERSION (1.0)
|
||||
"catalogs": [{"id": "corp", "url": "https://corp/catalog.json",
|
||||
"priority": 1, "install_policy": "install-allowed"}],
|
||||
}), encoding="utf-8")
|
||||
assert "corp" in {s.id for s in load_source_stack(tmp_path)}
|
||||
cfg.write_text(yaml.safe_dump({ # no schema_version key
|
||||
"catalogs": [{"id": "corp2", "url": "https://corp2/catalog.json",
|
||||
"priority": 1, "install_policy": "install-allowed"}],
|
||||
}), encoding="utf-8")
|
||||
assert "corp2" in {s.id for s in load_source_stack(tmp_path)}
|
||||
|
||||
|
||||
def test_project_config_overrides_same_id(tmp_path: Path):
|
||||
make_project(tmp_path)
|
||||
config = {
|
||||
|
||||
Reference in New Issue
Block a user