Skip to content
Closed
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
60 changes: 58 additions & 2 deletions .github/workflows/community-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,17 @@ jobs:
with:
python-version: "3.11"

- name: Install CLI from checkout
- name: Build CLI wheel from checkout
run: |
set -euo pipefail
uv build --wheel --out-dir /tmp/specify-community-smoke-dist
ls -l /tmp/specify-community-smoke-dist/*.whl

- name: Install CLI from built wheel
run: |
set -euo pipefail
uv venv /tmp/specify-community-smoke-venv
uv pip install --python /tmp/specify-community-smoke-venv/bin/python .
uv pip install --python /tmp/specify-community-smoke-venv/bin/python /tmp/specify-community-smoke-dist/*.whl

- name: Verify bundled init defaults
run: |
Expand All @@ -43,6 +49,20 @@ jobs:
test -f .specify/extensions/preview/extension.yml
test -f .specify/extensions/repository-governance/extension.yml
test -f .specify/presets/workflow-preset/preset.yml
/tmp/specify-community-smoke-venv/bin/python - <<'PY'
import json
from pathlib import Path

import yaml

manifest = yaml.safe_load(Path(".specify/presets/workflow-preset/preset.yml").read_text(encoding="utf-8"))
registry = json.loads(Path(".specify/presets/.registry").read_text(encoding="utf-8"))
manifest_version = manifest["preset"]["version"]
registry_version = registry["presets"]["workflow-preset"]["version"]
assert manifest_version == "1.3.1", manifest_version
assert registry_version == "1.3.1", registry_version
assert registry_version == manifest_version
PY
test -f .specify/presets/workflow-preset/templates/behavior/behavior-scenarios-draft.json
test -f .specify/presets/workflow-preset/templates/behavior/behavior-testability-checklist.md
test -f .specify/presets/workflow-preset/templates/behavior/bdd-draft.feature
Expand Down Expand Up @@ -91,6 +111,42 @@ jobs:
grep -q "Vertical Planner Agent" .claude/skills/speckit-implement/SKILL.md
grep -q "Worker Agent" .claude/skills/speckit-implement/SKILL.md

for extension_id in arch preview repository-governance; do
/tmp/specify-community-smoke-venv/bin/specify extension remove "$extension_id" --force
test ! -d ".specify/extensions/$extension_id"
case "$extension_id" in
arch)
test ! -d .claude/skills/speckit-arch-generate
test ! -d .claude/skills/speckit-arch-reverse
;;
preview)
test ! -d .claude/skills/speckit-preview-html
;;
repository-governance)
test ! -d .claude/skills/speckit-repository-governance-refresh
;;
esac
done

for extension_id in arch preview repository-governance; do
/tmp/specify-community-smoke-venv/bin/specify extension add "$GITHUB_WORKSPACE/extensions/$extension_id" --dev
case "$extension_id" in
arch)
test -f .claude/skills/speckit-arch-generate/SKILL.md
test -f .claude/skills/speckit-arch-reverse/SKILL.md
grep -q "4+1 architecture workflow" .claude/skills/speckit-arch-generate/SKILL.md
;;
preview)
test -f .claude/skills/speckit-preview-html/SKILL.md
grep -q "self-contained interactive HTML prototype" .claude/skills/speckit-preview-html/SKILL.md
;;
repository-governance)
test -f .claude/skills/speckit-repository-governance-refresh/SKILL.md
grep -q "Repository Governance Generate/Update" .claude/skills/speckit-repository-governance-refresh/SKILL.md
;;
esac
done

/tmp/specify-community-smoke-venv/bin/specify preset resolve plan-template | tee resolve-plan-template.txt
grep -q "workflow-preset" resolve-plan-template.txt
grep -q "Composition chain" resolve-plan-template.txt
10 changes: 10 additions & 0 deletions tests/integrations/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,12 @@ def test_shared_template_writes_are_not_world_writable(self, tmp_path):
"""Shared template writes use a safe default mode instead of chmod 666."""
from specify_cli.shared_infra import install_shared_infra

probe = tmp_path / "mode-probe"
probe.write_text("x", encoding="utf-8")
probe.chmod(0o644)
if probe.stat().st_mode & 0o777 != 0o644:
pytest.skip("current filesystem does not preserve POSIX mode bits")

project = tmp_path / "template-mode-test"
project.mkdir()

Expand Down Expand Up @@ -985,6 +991,10 @@ def test_workflow_preset_registers_commands_and_composes_wrappers(self, tmp_path

preset_registry = json.loads((project / ".specify" / "presets" / ".registry").read_text())
workflow_entry = preset_registry["presets"]["workflow-preset"]
installed_manifest = yaml.safe_load((preset_dir / "preset.yml").read_text(encoding="utf-8"))
assert installed_manifest["preset"]["version"] == "1.3.1"
assert workflow_entry["version"] == "1.3.1"
assert workflow_entry["version"] == installed_manifest["preset"]["version"]
expected_preset_commands = {
"speckit.specify",
"speckit.clarify",
Expand Down
65 changes: 65 additions & 0 deletions tests/test_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3524,6 +3524,71 @@ def test_workflow_preset_integration_release_payload_contract(self):
assert 'grep -F "(top layer from: core)" preset-resolve-tasks-template.txt' in workflow_text
assert "test -f .specify/templates/tasks-template.md" in workflow_text

def test_community_smoke_checks_wheel_assets_and_extension_dev_reinstall(self):
"""Community smoke validates wheel assets and extension dev reinstall skills."""
workflow_path = (
Path(__file__).parent.parent
/ ".github"
/ "workflows"
/ "community-smoke.yml"
)
workflow_text = workflow_path.read_text(encoding="utf-8")
workflow = yaml.safe_load(workflow_text)
on_config = workflow.get("on", workflow.get(True))
steps = workflow["jobs"]["init-smoke"]["steps"]
runs_by_name = {
step["name"]: step["run"]
for step in steps
if "name" in step and "run" in step
}
build_run = runs_by_name["Build CLI wheel from checkout"]
install_run = runs_by_name["Install CLI from built wheel"]
verify_run = runs_by_name["Verify bundled init defaults"]

assert "pull_request" in on_config
assert "uv build --wheel --out-dir /tmp/specify-community-smoke-dist" in build_run
assert (
"uv pip install --python /tmp/specify-community-smoke-venv/bin/python "
"/tmp/specify-community-smoke-dist/*.whl"
) in install_run
assert (
'manifest = yaml.safe_load(Path(".specify/presets/workflow-preset/preset.yml")'
'.read_text(encoding="utf-8"))'
) in verify_run
assert (
'registry = json.loads(Path(".specify/presets/.registry").read_text(encoding="utf-8"))'
) in verify_run
assert 'manifest_version == "1.3.1"' in verify_run
assert 'registry_version == "1.3.1"' in verify_run
assert "registry_version == manifest_version" in verify_run
assert (
'for extension_id in arch preview repository-governance; do'
in verify_run
)
assert (
'/tmp/specify-community-smoke-venv/bin/specify extension remove "$extension_id" --force'
in verify_run
)
assert "test ! -d .claude/skills/speckit-arch-generate" in verify_run
assert "test ! -d .claude/skills/speckit-arch-reverse" in verify_run
assert "test ! -d .claude/skills/speckit-preview-html" in verify_run
assert (
"test ! -d .claude/skills/speckit-repository-governance-refresh"
in verify_run
)
assert (
'/tmp/specify-community-smoke-venv/bin/specify extension add "$GITHUB_WORKSPACE/extensions/$extension_id" --dev'
in verify_run
)
assert (
'test -f .claude/skills/speckit-arch-generate/SKILL.md'
in verify_run
)
assert (
'test -f .claude/skills/speckit-repository-governance-refresh/SKILL.md'
in verify_run
)

def test_bundled_preset_download_raises_error(self, project_dir):
"""download_pack raises PresetError for bundled presets without download_url."""
catalog = PresetCatalog(project_dir)
Expand Down
24 changes: 19 additions & 5 deletions tests/test_setup_arch.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ def _powershell_script_arg(exe: str, script: Path) -> str:
return str(script)


def _run_powershell(args: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]:
env = _clean_env()
env.setdefault("NO_COLOR", "1")
result = subprocess.run(
args,
cwd=cwd,
capture_output=True,
check=False,
env=env,
)
return subprocess.CompletedProcess(
result.args,
result.returncode,
result.stdout.decode("utf-8", errors="replace"),
result.stderr.decode("utf-8", errors="replace"),
)


@pytest.fixture
def arch_repo(tmp_path: Path) -> Path:
repo = tmp_path / "proj"
Expand Down Expand Up @@ -150,13 +168,9 @@ def test_setup_arch_bash_preserves_existing_files(arch_repo: Path) -> None:
def test_setup_arch_powershell_creates_all_artifacts_and_json(arch_repo: Path) -> None:
script = arch_repo / ".specify" / "extensions" / "arch" / "scripts" / "powershell" / "setup-arch.ps1"
exe = "pwsh" if HAS_PWSH else _POWERSHELL
result = subprocess.run(
result = _run_powershell(
[exe, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", _powershell_script_arg(exe, script), "-Json"],
cwd=arch_repo,
capture_output=True,
text=True,
check=False,
env=_clean_env(),
)

assert result.returncode == 0, result.stderr + result.stdout
Expand Down
90 changes: 78 additions & 12 deletions tests/test_setup_plan_feature_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import shutil
import subprocess
import sys
from pathlib import Path

import pytest
Expand Down Expand Up @@ -56,6 +57,77 @@ def _clean_env() -> dict[str, str]:
return env


def _is_windows_powershell(exe: str) -> bool:
return (
sys.platform != "win32"
and str(exe).endswith("powershell.exe")
and shutil.which("wslpath") is not None
)


def _to_windows_path(path: Path) -> str:
result = subprocess.run(
["wslpath", "-w", str(path)],
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()


def _quote_ps(value: str) -> str:
return "'" + value.replace("'", "''") + "'"


def _run_powershell(args: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]:
env = _clean_env()
env.setdefault("NO_COLOR", "1")
run_args = list(args)
if args and _is_windows_powershell(args[0]):
exe, rest = args[0], args[1:]
cwd_command = f"Set-Location -LiteralPath {_quote_ps(_to_windows_path(cwd))}"
if "-File" in rest:
index = rest.index("-File")
script = rest[index + 1]
script_args = rest[index + 2 :]
command = f"{cwd_command}; & {_quote_ps(script)}"
if script_args:
command += " " + " ".join(script_args)
run_args = [exe, *rest[:index], "-Command", command]
elif "-Command" in rest:
index = rest.index("-Command")
command = f"{cwd_command}; {rest[index + 1]}"
run_args = [exe, *rest[:index], "-Command", command, *rest[index + 2 :]]

result = subprocess.run(
run_args,
cwd=cwd,
capture_output=True,
check=False,
env=env,
)
return subprocess.CompletedProcess(
result.args,
result.returncode,
result.stdout.decode("utf-8", errors="replace"),
result.stderr.decode("utf-8", errors="replace"),
)


def _powershell_script_arg(exe: str, script: Path) -> str:
if _is_windows_powershell(exe):
return _to_windows_path(script)
return str(script)


def _powershell_has_git(exe: str, cwd: Path) -> bool:
result = _run_powershell(
[exe, "-NoProfile", "-Command", "git rev-parse --is-inside-work-tree"],
cwd=cwd,
)
return result.returncode == 0 and "true" in result.stdout.lower()


def _git_init(repo: Path) -> None:
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
subprocess.run(
Expand Down Expand Up @@ -167,13 +239,9 @@ def test_setup_plan_ps_passes_custom_branch_when_feature_json_valid(plan_repo: P
)
script = plan_repo / ".specify" / "scripts" / "powershell" / "setup-plan.ps1"
exe = "pwsh" if HAS_PWSH else _POWERSHELL
result = subprocess.run(
[exe, "-NoProfile", "-File", str(script)],
result = _run_powershell(
[exe, "-NoProfile", "-File", _powershell_script_arg(exe, script)],
cwd=plan_repo,
capture_output=True,
text=True,
check=False,
env=_clean_env(),
)
assert result.returncode == 0, result.stderr + result.stdout
assert (feat / "plan.md").is_file()
Expand All @@ -190,13 +258,11 @@ def test_setup_plan_ps_fails_custom_branch_without_feature_json(
)
script = plan_repo / ".specify" / "scripts" / "powershell" / "setup-plan.ps1"
exe = "pwsh" if HAS_PWSH else _POWERSHELL
result = subprocess.run(
[exe, "-NoProfile", "-File", str(script)],
if not _powershell_has_git(exe, plan_repo):
pytest.skip("PowerShell cannot access git in this environment")
result = _run_powershell(
[exe, "-NoProfile", "-File", _powershell_script_arg(exe, script)],
cwd=plan_repo,
capture_output=True,
text=True,
check=False,
env=_clean_env(),
)
assert result.returncode != 0
assert "Not on a feature branch" in result.stderr
Loading
Loading