Skip to content

Commit feee40f

Browse files
committed
Bundle speckit workflow: auto-install during specify init
- Add workflows/speckit to pyproject.toml force-include for wheel builds - Add _locate_bundled_workflow() helper (mirrors _locate_bundled_extension) - Auto-install speckit workflow during specify init (after git extension) - Update all integration file inventory tests to expect workflow files
1 parent b56d42b commit feee40f

7 files changed

Lines changed: 78 additions & 0 deletions

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ packages = ["src/specify_cli"]
4141
"scripts/powershell" = "specify_cli/core_pack/scripts/powershell"
4242
# Bundled extensions (installable via `specify extension add <name>`)
4343
"extensions/git" = "specify_cli/core_pack/extensions/git"
44+
# Bundled workflows (auto-installed during `specify init`)
45+
"workflows/speckit" = "specify_cli/core_pack/workflows/speckit"
4446

4547
[project.optional-dependencies]
4648
test = [

src/specify_cli/__init__.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,31 @@ def _locate_bundled_extension(extension_id: str) -> Path | None:
621621
return None
622622

623623

624+
def _locate_bundled_workflow(workflow_id: str) -> Path | None:
625+
"""Return the path to a bundled workflow directory, or None.
626+
627+
Checks the wheel's core_pack first, then falls back to the
628+
source-checkout ``workflows/<id>/`` directory.
629+
"""
630+
import re as _re
631+
if not _re.match(r'^[a-z0-9-]+$', workflow_id):
632+
return None
633+
634+
core = _locate_core_pack()
635+
if core is not None:
636+
candidate = core / "workflows" / workflow_id
637+
if (candidate / "workflow.yml").is_file():
638+
return candidate
639+
640+
# Source-checkout / editable install: look relative to repo root
641+
repo_root = Path(__file__).parent.parent.parent
642+
candidate = repo_root / "workflows" / workflow_id
643+
if (candidate / "workflow.yml").is_file():
644+
return candidate
645+
646+
return None
647+
648+
624649
def _install_shared_infra(
625650
project_path: Path,
626651
script_type: str,
@@ -1134,6 +1159,7 @@ def init(
11341159
("chmod", "Ensure scripts executable"),
11351160
("constitution", "Constitution setup"),
11361161
("git", "Install git extension"),
1162+
("workflow", "Install bundled workflow"),
11371163
("final", "Finalize"),
11381164
]:
11391165
tracker.add(key, label)
@@ -1237,6 +1263,37 @@ def init(
12371263
else:
12381264
tracker.skip("git", "--no-git flag")
12391265

1266+
# Install bundled speckit workflow
1267+
try:
1268+
bundled_wf = _locate_bundled_workflow("speckit")
1269+
if bundled_wf:
1270+
from .workflows.catalog import WorkflowRegistry
1271+
from .workflows.engine import WorkflowDefinition
1272+
wf_registry = WorkflowRegistry(project_path)
1273+
if wf_registry.is_installed("speckit"):
1274+
tracker.complete("workflow", "already installed")
1275+
else:
1276+
import shutil as _shutil
1277+
dest_wf = project_path / ".specify" / "workflows" / "speckit"
1278+
dest_wf.mkdir(parents=True, exist_ok=True)
1279+
_shutil.copy2(
1280+
bundled_wf / "workflow.yml",
1281+
dest_wf / "workflow.yml",
1282+
)
1283+
definition = WorkflowDefinition.from_yaml(dest_wf / "workflow.yml")
1284+
wf_registry.add("speckit", {
1285+
"name": definition.name,
1286+
"version": definition.version,
1287+
"description": definition.description,
1288+
"source": "bundled",
1289+
})
1290+
tracker.complete("workflow", "speckit installed")
1291+
else:
1292+
tracker.skip("workflow", "bundled workflow not found")
1293+
except Exception as wf_err:
1294+
sanitized_wf = str(wf_err).replace('\n', ' ').strip()
1295+
tracker.error("workflow", f"install failed: {sanitized_wf[:120]}")
1296+
12401297
# Fix permissions after all installs (scripts + extensions)
12411298
ensure_executable_scripts(project_path, tracker=tracker)
12421299

tests/integrations/test_integration_base_markdown.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,9 @@ def _expected_files(self, script_variant: str) -> list[str]:
245245
files.append(f".specify/templates/{name}")
246246

247247
files.append(".specify/memory/constitution.md")
248+
# Bundled workflow
249+
files.append(".specify/workflows/speckit/workflow.yml")
250+
files.append(".specify/workflows/workflow-registry.json")
248251
return sorted(files)
249252

250253
def test_complete_file_inventory_sh(self, tmp_path):

tests/integrations/test_integration_base_skills.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,11 @@ def _expected_files(self, script_variant: str) -> list[str]:
347347
".specify/templates/spec-template.md",
348348
".specify/templates/tasks-template.md",
349349
]
350+
# Bundled workflow
351+
files += [
352+
".specify/workflows/speckit/workflow.yml",
353+
".specify/workflows/workflow-registry.json",
354+
]
350355
return sorted(files)
351356

352357
def test_complete_file_inventory_sh(self, tmp_path):

tests/integrations/test_integration_base_toml.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,9 @@ def _expected_files(self, script_variant: str) -> list[str]:
445445
files.append(f".specify/templates/{name}")
446446

447447
files.append(".specify/memory/constitution.md")
448+
# Bundled workflow
449+
files.append(".specify/workflows/speckit/workflow.yml")
450+
files.append(".specify/workflows/workflow-registry.json")
448451
return sorted(files)
449452

450453
def test_complete_file_inventory_sh(self, tmp_path):

tests/integrations/test_integration_copilot.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ def test_complete_file_inventory_sh(self, tmp_path):
199199
".specify/templates/spec-template.md",
200200
".specify/templates/tasks-template.md",
201201
".specify/memory/constitution.md",
202+
".specify/workflows/speckit/workflow.yml",
203+
".specify/workflows/workflow-registry.json",
202204
])
203205
assert actual == expected, (
204206
f"Missing: {sorted(set(expected) - set(actual))}\n"
@@ -259,6 +261,8 @@ def test_complete_file_inventory_ps(self, tmp_path):
259261
".specify/templates/spec-template.md",
260262
".specify/templates/tasks-template.md",
261263
".specify/memory/constitution.md",
264+
".specify/workflows/speckit/workflow.yml",
265+
".specify/workflows/workflow-registry.json",
262266
])
263267
assert actual == expected, (
264268
f"Missing: {sorted(set(expected) - set(actual))}\n"

tests/integrations/test_integration_generic.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ def test_complete_file_inventory_sh(self, tmp_path):
248248
".specify/templates/plan-template.md",
249249
".specify/templates/spec-template.md",
250250
".specify/templates/tasks-template.md",
251+
".specify/workflows/speckit/workflow.yml",
252+
".specify/workflows/workflow-registry.json",
251253
])
252254
assert actual == expected, (
253255
f"Missing: {sorted(set(expected) - set(actual))}\n"
@@ -304,6 +306,8 @@ def test_complete_file_inventory_ps(self, tmp_path):
304306
".specify/templates/plan-template.md",
305307
".specify/templates/spec-template.md",
306308
".specify/templates/tasks-template.md",
309+
".specify/workflows/speckit/workflow.yml",
310+
".specify/workflows/workflow-registry.json",
307311
])
308312
assert actual == expected, (
309313
f"Missing: {sorted(set(expected) - set(actual))}\n"

0 commit comments

Comments
 (0)