Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions extensions/git/extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ extension:
author: spec-kit-core
repository: https://github.com/github/spec-kit
license: MIT
install_notice: |
The git extension is currently enabled by default, but starting with
v1.0.0 it will require explicit opt-in.
To opt in after v1.0.0:
Comment on lines +13 to +15
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notice text says “starting with v1.0.0…”, but this file also sets the extension version to 1.0.0. That makes it ambiguous whether the message refers to spec-kit/specify-cli v1.0.0 or the git extension’s own version. Consider explicitly naming the product/version (e.g., “spec-kit v1.0.0” or “specify-cli v1.0.0”) to avoid user confusion.

Suggested change
v1.0.0 it will require explicit opt-in.
To opt in after v1.0.0:
specify-cli v1.0.0 it will require explicit opt-in.
To opt in after specify-cli v1.0.0:

Copilot uses AI. Check for mistakes.
• specify init --extension git
• specify extension add git (post-init)
requires:
speckit_version: ">=0.2.0"
Expand Down
17 changes: 16 additions & 1 deletion src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1277,6 +1277,8 @@ def init(

ensure_constitution_from_template(project_path, tracker=tracker)

_git_ext_freshly_installed = False
_git_ext_install_notice: str | None = None
if not no_git:
tracker.start("git")
git_messages = []
Expand Down Expand Up @@ -1307,10 +1309,12 @@ def init(
if manager.registry.is_installed("git"):
git_messages.append("extension already installed")
else:
manager.install_from_directory(
ext_manifest = manager.install_from_directory(
bundled_path, get_speckit_version()
)
git_messages.append("extension installed")
_git_ext_freshly_installed = True
_git_ext_install_notice = ext_manifest.install_notice
else:
git_has_error = True
git_messages.append("bundled extension not found")
Expand Down Expand Up @@ -1454,6 +1458,17 @@ def init(
console.print(tracker.render())
console.print("\n[bold green]Project ready.[/bold green]")

if _git_ext_freshly_installed and _git_ext_install_notice:
console.print()
console.print(
Panel(
_git_ext_install_notice.strip(),
title="[yellow]⚠ Deprecation notice: git Extension[/yellow]",
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The panel title capitalizes “Extension” in “git Extension”, which reads awkwardly/inconsistently with the rest of the CLI messaging (and with the body text’s “git extension”). Consider changing the title casing to “git extension” or “Git extension” for a more polished, consistent user-facing warning.

Suggested change
title="[yellow]⚠ Deprecation notice: git Extension[/yellow]",
title="[yellow]⚠ Deprecation notice: git extension[/yellow]",

Copilot uses AI. Check for mistakes.
border_style="yellow",
padding=(1, 2),
)
)
Comment thread
aaronrsun marked this conversation as resolved.

# Agent folder security notice
agent_config = AGENT_CONFIG.get(selected_ai)
if agent_config:
Expand Down
9 changes: 9 additions & 0 deletions src/specify_cli/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,15 @@ def hooks(self) -> Dict[str, Any]:
"""Get hook definitions."""
return self.data.get("hooks", {})

@property
def install_notice(self) -> str | None:
"""Get optional install notice message.

Extensions can specify an 'install_notice' field to display
important information to users when the extension is first installed.
"""
return self.data.get("extension", {}).get("install_notice")
Comment thread
aaronrsun marked this conversation as resolved.

def get_hash(self) -> str:
"""Calculate SHA256 hash of manifest file."""
with open(self.path, 'rb') as f:
Expand Down
115 changes: 115 additions & 0 deletions tests/extensions/git/test_git_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,3 +837,118 @@ def test_test_feature_branch_accepts_single_prefix(self, tmp_path: Path):
text=True,
)
assert result.returncode == 0


# ── Deprecation Notice Tests ──────────────────────────────────────────────────


class TestGitExtDeprecationNotice:
"""Tests for the v1.0.0 deprecation notice shown during specify init."""

def test_deprecation_notice_shown_on_fresh_install(self, tmp_path: Path):
"""specify init shows the git extension deprecation notice on first install."""
from typer.testing import CliRunner
from unittest.mock import patch, MagicMock
from specify_cli import app

project_dir = tmp_path / "test-project"
runner = CliRunner()

mock_manifest = MagicMock()
mock_manifest.install_notice = (
"The git extension is currently enabled by default, but starting with\n"
"v1.0.0 it will require explicit opt-in.\n\n"
"To opt in after v1.0.0:\n"
" • specify init --extension git\n"
" • specify extension add git (post-init)"
)

mock_registry = MagicMock()
mock_registry.is_installed.return_value = False

mock_manager = MagicMock()
mock_manager.registry = mock_registry
mock_manager.install_from_directory.return_value = mock_manifest

with patch("specify_cli.extensions.ExtensionManager", return_value=mock_manager):
result = runner.invoke(
app,
["init", str(project_dir), "--ai", "claude", "--ignore-agent-tools", "--script", "sh"],
catch_exceptions=False,
)
Comment thread
aaronrsun marked this conversation as resolved.

assert result.exit_code == 0, result.output
assert "Deprecation notice: git Extension" in result.output
assert "v1.0.0" in result.output
assert "specify extension add git" in result.output
Comment on lines +880 to +883
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These assertions check result.output directly for text that is rendered by Rich (Panel + colored title). That output can include ANSI escape codes depending on the test runner/terminal settings, making the substring checks flaky. Consider normalizing the output first (e.g., via tests.conftest.strip_ansi, as done in other CLI tests) before asserting on the notice text.

Copilot uses AI. Check for mistakes.

def test_deprecation_notice_not_shown_when_already_installed(self, tmp_path: Path):
"""specify init does NOT show the deprecation notice when git extension is already installed."""
from typer.testing import CliRunner
from unittest.mock import patch, MagicMock
from specify_cli import app

project_dir = tmp_path / "test-project"
runner = CliRunner()

mock_registry = MagicMock()
mock_registry.is_installed.return_value = True

mock_manager = MagicMock()
mock_manager.registry = mock_registry

with patch("specify_cli.extensions.ExtensionManager", return_value=mock_manager):
result = runner.invoke(
app,
["init", str(project_dir), "--ai", "claude", "--ignore-agent-tools", "--script", "sh"],
catch_exceptions=False,
)

assert result.exit_code == 0, result.output
assert "Deprecation notice: git Extension" not in result.output

def test_deprecation_notice_not_shown_with_no_git_flag(self, tmp_path: Path):
"""specify init does NOT show the deprecation notice when --no-git is passed."""
from typer.testing import CliRunner
from specify_cli import app

project_dir = tmp_path / "test-project"
runner = CliRunner()

result = runner.invoke(
app,
["init", str(project_dir), "--ai", "claude", "--ignore-agent-tools", "--no-git", "--script", "sh"],
catch_exceptions=False,
)

assert result.exit_code == 0, result.output
assert "Deprecation notice: git Extension" not in result.output

def test_deprecation_notice_not_shown_when_no_install_notice(self, tmp_path: Path):
"""specify init does NOT show the deprecation notice if extension has no install_notice."""
from typer.testing import CliRunner
from unittest.mock import patch, MagicMock
from specify_cli import app

project_dir = tmp_path / "test-project"
runner = CliRunner()

mock_manifest = MagicMock()
mock_manifest.install_notice = None # No notice defined

mock_registry = MagicMock()
mock_registry.is_installed.return_value = False

mock_manager = MagicMock()
mock_manager.registry = mock_registry
mock_manager.install_from_directory.return_value = mock_manifest

with patch("specify_cli.extensions.ExtensionManager", return_value=mock_manager):
result = runner.invoke(
app,
["init", str(project_dir), "--ai", "claude", "--ignore-agent-tools", "--script", "sh"],
catch_exceptions=False,
)

assert result.exit_code == 0, result.output
assert "Deprecation notice: git Extension" not in result.output