fix: allow prerelease spec-kit versions in compatibility checks (#2695)

* docs: generate integrations reference from catalog

* refactor: integrate table rendering into specify integration search --markdown

- Remove standalone scripts/generate_integrations_reference.py
- Strip doc injection machinery from catalog_docs.py; keep only table rendering
- Wire render_integrations_table() into existing --markdown flag of integration search
- Remove old simple markdown table block from integration_search (was Name|ID|Version|Description|Author)
- Simplify tests: drop subprocess/doc-path tests, keep table rendering and metadata tests
- Clean up docs/reference/integrations.md: remove generated markers, update note

* fix: address Copilot review feedback on catalog_docs and integration_search

- Warn when --markdown is combined with filters (query/--tag/--author) which are
  silently ignored; catch ValueError/FileNotFoundError and surface clean error
  via console instead of raw traceback (r3244821516)
- Add coverage enforcement in list_integrations_for_docs(): raises ValueError
  with actionable message if any registry key is missing from INTEGRATION_DOC_URLS,
  preventing silently incomplete doc tables (r3244821589)
- Rename test to accurately reflect sources: label derives from registry config,
  URL comes from INTEGRATION_DOC_URLS doc map — not solely from registry (r3244821607)
- Simplify test dict construction to idiomatic dict comprehension (r3244821619)

* fix: add sync test, INTEGRATIONS_REFERENCE_PATH constant, and fix naming

* revert: restore docs/reference/integrations.md to upstream/main; remove sync test (GH Actions job will handle)

* fix: remove dead INTEGRATIONS_REFERENCE_PATH, drop URL-length padding, fix docstring, drop FileNotFoundError

* fix: send --markdown warnings/errors to stderr, rename test for clarity

* fix: detect stale doc-map keys, test _render_cell escaping, strengthen header assertion

* refactor: promote _render_cell to public render_cell function

* test: mock registry and doc maps to avoid brittle live registry coupling

* refactor: flatten patches, remove unused imports, fix trailing whitespace, optimize missing calculation

* refactor: make validation non-fatal, fix context manager syntax, add CLI tests

* fix: improve docstring clarity, test robustness, and exception handling

* fix: improve test assertions, disable warnings by default, enhance exception handling

* fix: make CLI tests deterministic and improve config access resilience

* fix: remove extra blank line, add stale keys validation, add regression test for docs sync

* Fix 5 remaining feedback items:
- Rename _get_mocked_cli_runner() to _get_catalog_docs_patches() for clarity
- Use ExitStack context manager for guaranteed patch cleanup
- Add explicit UTF-8 encoding to file reads
- Skip doc sync test gracefully when docs aren't present
- Remove exception chaining from typer.Exit to avoid noisy tracebacks

* address all outstanding copilot review feedback on PR 2563

* Address Copilot feedback: escape URLs in markdown links, deduplicate cell rendering, fix table parser for escaped pipes

* Address 3 new Copilot feedback: add URL escaping test, fix parse_first_markdown_table for escaped pipes, guard community tests with skip

* Address 3 new Copilot feedback: escape id field, remove unused alias, escape integration URLs

* Address 3 new Copilot feedback: fix comment name, include all integrations in list

* Fix architectural issue: escape raw fields before composing Markdown to prevent double-escaping

* Deduplicate _escape_url_for_markdown_link and add URL escaping test

* Address 4 new Copilot feedback: add trailing newline, fix test helper ExitStack, update warning message

* Address 4 new Copilot feedback: make escape function public, fix error message, validate test rows, prevent double newline

* Update error message in test_missing_catalog_file for clarity

* Remove obsolete integrations sync test

* keep integrations docs in sync

* fix: allow prerelease spec-kit versions in compatibility checks

Allow prerelease/dev builds to satisfy extension and preset compatibility
checks when their version number falls within the required specifier range.
Also harden the integrations docs rendering helpers and add regression
coverage for the markdown table parsing and version gating paths.

Tests: pytest -q; python3 -m compileall -q .; black/flake8 unavailable
Reference: branch 002-generate-integrations-docs; source patch /tmp/spec-kit-changes.patch

* fix: isolate prerelease compatibility gate changes

Keep the prerelease/version compatibility fix on its own branch and remove
the unrelated integrations docs updates that belong with PR 2563.

Tests: full suite passed on the prerelease branch before splitting; docs branch covered by targeted docs tests
Reference: upstream/main; source patch /tmp/spec-kit-changes.patch

* Address PR 2695 feedback: Centralize prerelease policy and add boundary test

* Address remaining Copilot PR feedback: revert docs and add preset prerelease tests

* Remove unreachable raise CompatibilityError

* Fix PEP8 E302 and E303 formatting issues
This commit is contained in:
Dyan Galih
2026-06-30 21:41:57 +07:00
committed by GitHub
parent 5e72b1d486
commit 6fb7e77b3e
6 changed files with 65 additions and 40 deletions

View File

@@ -528,7 +528,6 @@ def _require_specify_project() -> Path:
raise typer.Exit(1)
# ===== Preset Commands =====
# Moved to presets/_commands.py — registered here to preserve CLI surface.

View File

@@ -304,3 +304,27 @@ def _display_project_path(project_root: Path, path: str | Path) -> str:
except (OSError, ValueError):
return path_obj.as_posix()
return rel_path.as_posix()
def version_satisfies(current: str, required: str) -> bool:
"""Check if current version satisfies required version specifier.
Evaluates the version against the specifier using the project's
prerelease policy (prereleases are allowed).
Args:
current: Current version (e.g., "0.1.5")
required: Required version specifier (e.g., ">=0.1.0,<2.0.0")
Returns:
True if version satisfies requirement
"""
from packaging import version as pkg_version
from packaging.specifiers import InvalidSpecifier, SpecifierSet
try:
current_ver = pkg_version.Version(current)
specifier = SpecifierSet(required)
return specifier.contains(current_ver, prereleases=True)
except (pkg_version.InvalidVersion, InvalidSpecifier):
return False

View File

@@ -28,7 +28,7 @@ from packaging.specifiers import InvalidSpecifier, SpecifierSet
from .._init_options import is_ai_skills_enabled
from .._invocation_style import is_dollar_skills_agent, is_slash_skills_agent
from .._utils import dump_frontmatter, relative_extension_path_violation
from .._utils import dump_frontmatter, relative_extension_path_violation, version_satisfies
from ..catalogs import CatalogEntry as BaseCatalogEntry
from ..catalogs import CatalogStackBase
from ..shared_infra import verify_archive_sha256
@@ -1279,20 +1279,20 @@ class ExtensionManager:
CompatibilityError: If extension is incompatible
"""
required = manifest.requires_speckit_version
current = pkg_version.Version(speckit_version)
# Parse version specifier (e.g., ">=0.1.0,<2.0.0")
try:
specifier = SpecifierSet(required)
if current not in specifier:
raise CompatibilityError(
f"Extension requires spec-kit {required}, "
f"but {speckit_version} is installed.\n"
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
)
SpecifierSet(required) # Just to validate
except InvalidSpecifier:
raise CompatibilityError(f"Invalid version specifier: {required}")
if not version_satisfies(speckit_version, required):
raise CompatibilityError(
f"Extension requires spec-kit {required}, "
f"but {speckit_version} is installed.\n"
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
)
return True
def install_from_directory(
@@ -1871,24 +1871,6 @@ class ExtensionManager:
return None
def version_satisfies(current: str, required: str) -> bool:
"""Check if current version satisfies required version specifier.
Args:
current: Current version (e.g., "0.1.5")
required: Required version specifier (e.g., ">=0.1.0,<2.0.0")
Returns:
True if version satisfies requirement
"""
try:
current_ver = pkg_version.Version(current)
specifier = SpecifierSet(required)
return current_ver in specifier
except (pkg_version.InvalidVersion, InvalidSpecifier):
return False
class CommandRegistrar:
"""Handles registration of extension commands with AI agents.

View File

@@ -30,7 +30,7 @@ from packaging.specifiers import SpecifierSet, InvalidSpecifier
from ..extensions import REINSTALL_COMMAND, ExtensionRegistry, normalize_priority
from .._init_options import is_ai_skills_enabled
from ..integrations.base import IntegrationBase
from .._utils import dump_frontmatter
from .._utils import dump_frontmatter, version_satisfies
from ..shared_infra import verify_archive_sha256
@@ -572,19 +572,16 @@ class PresetManager:
PresetCompatibilityError: If pack is incompatible
"""
required = manifest.requires_speckit_version
current = pkg_version.Version(speckit_version)
try:
specifier = SpecifierSet(required)
if current not in specifier:
raise PresetCompatibilityError(
f"Preset requires spec-kit {required}, "
f"but {speckit_version} is installed.\n"
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
)
SpecifierSet(required) # Just to validate
except InvalidSpecifier:
raise PresetCompatibilityError(f"Invalid version specifier: {required}")
if not version_satisfies(speckit_version, required):
raise PresetCompatibilityError(
f"Invalid version specifier: {required}"
f"Preset requires spec-kit {required}, "
f"but {speckit_version} is installed.\n"
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
)
return True

View File

@@ -37,8 +37,8 @@ from specify_cli.extensions import (
ValidationError,
CompatibilityError,
normalize_priority,
version_satisfies,
)
from specify_cli._utils import version_satisfies
# Minimal valid ZIP (empty end-of-central-directory record). Passes
# zipfile.is_zipfile() so --from download tests exercise the content guard.
@@ -1005,6 +1005,14 @@ class TestExtensionManager:
with pytest.raises(CompatibilityError, match="Extension requires spec-kit"):
manager.check_compatibility(manifest, "0.0.1")
def test_check_compatibility_allows_prerelease_builds(self, extension_dir, project_dir):
"""Prerelease spec-kit builds should satisfy compatible version ranges."""
manager = ExtensionManager(project_dir)
manifest = ExtensionManifest(extension_dir / "extension.yml")
result = manager.check_compatibility(manifest, "0.8.8.dev0")
assert result is True
def test_install_from_directory(self, extension_dir, project_dir):
"""Test installing extension from directory."""
manager = ExtensionManager(project_dir)
@@ -2629,6 +2637,12 @@ class TestVersionSatisfies:
assert version_satisfies("1.0.5", ">=1.0.0,!=1.0.3")
assert not version_satisfies("1.0.3", ">=1.0.0,!=1.0.3")
def test_version_satisfies_prerelease(self):
"""Prerelease builds should satisfy compatible lower bounds, but not higher bounds."""
assert version_satisfies("0.8.8.dev0", ">=0.2.0")
assert not version_satisfies("0.2.0.dev0", ">=0.2.0")
assert not version_satisfies("0.8.7.dev1", ">=0.8.8")
def test_version_satisfies_invalid(self):
"""Test invalid version strings."""
assert not version_satisfies("invalid", ">=1.0.0")

View File

@@ -710,6 +710,15 @@ class TestPresetManager:
manifest = PresetManifest(pack_dir / "preset.yml")
assert manager.check_compatibility(manifest, "0.1.5") is True
def test_check_compatibility_prerelease(self, pack_dir, temp_dir):
"""Test compatibility check allows prereleases and fails on boundary."""
manager = PresetManager(temp_dir)
manifest = PresetManifest(pack_dir / "preset.yml")
# manifest requires >=0.1.0
assert manager.check_compatibility(manifest, "0.8.8.dev0") is True
with pytest.raises(PresetCompatibilityError, match="Preset requires spec-kit"):
manager.check_compatibility(manifest, "0.1.0.dev0")
def test_check_compatibility_invalid(self, pack_dir, temp_dir):
"""Test compatibility check with invalid specifier."""
manager = PresetManager(temp_dir)