feat(scripts): add SPECIFY_INIT_DIR to target a member project from the repo root (#2892)

* feat(scripts): add SPECIFY_INIT_DIR to target a member project from the repo root

Resolve an explicit SPECIFY_INIT_DIR project override once in the core
get_repo_root / Get-RepoRoot, so a non-interactive / CI caller can target a
member project (the directory containing .specify/) from a monorepo root
without cd. Strict by design: the path must exist and contain .specify/,
otherwise it hard-errors with no silent fallback.

- Single resolver in core; the git feature-branch script inherits it by
  sourcing core, with no per-extension copies.
- PS resolver verifies the resolved path is a directory (Resolve-Path also
  succeeds for files) so a file value errors as "not an existing directory".
- get_feature_paths splits decl/assignment so a SPECIFY_INIT_DIR failure
  propagates instead of being masked by `local`.
- create-new-feature-branch: when core is absent (only git-common loaded) and
  SPECIFY_INIT_DIR is set, hard-error rather than silently using the git root.
- Document SPECIFY_INIT_DIR and SPECIFY_FEATURE_DIRECTORY in the core reference.
- Tests for valid/relative/trailing-slash/file/missing/no-.specify targets,
  feature-axis composition, the no-core guard, and a PowerShell mirror.

* fix: guard SPECIFY_INIT_DIR with stale core scripts

* docs: clarify SPECIFY_FEATURE_DIRECTORY precedence wording

* fix: normalize trailing slash in PowerShell SPECIFY_INIT_DIR resolver

Resolve-Path preserves a trailing separator from its input, so a
SPECIFY_INIT_DIR ending in a slash returned a root that didn't match the
bash resolver (whose `cd && pwd` strips it). That broke
test_ps_trailing_slash_tolerated on the CI runners, which do have pwsh.
Trim it with TrimEndingDirectorySeparator (no-op on a bare root or a path
with no trailing separator).

Also fix the misleading test comment: the PowerShell mirror runs on the
CI ubuntu/windows runners (they ship pwsh), it is not skipped there.

* test: normalize bash path expectations on Windows

* docs: clarify SPECIFY_INIT_DIR root helpers
This commit is contained in:
Pascal THUET
2026-06-19 19:05:42 +02:00
committed by GitHub
parent 46ade96a27
commit a17a658bbd
9 changed files with 644 additions and 7 deletions

View File

@@ -382,6 +382,36 @@ class TestCreateFeatureBash:
assert data.get("DRY_RUN") is True
assert not (project / "specs" / data["BRANCH_NAME"]).exists()
def test_specify_init_dir_without_core_errors(self, tmp_path: Path):
"""With no core scripts (only git-common.sh loaded), a set SPECIFY_INIT_DIR
hard-errors instead of silently falling back to the walk-up project root."""
project = _setup_project(tmp_path, git=False)
# Simulate a no-core install: drop core common.sh so only git-common.sh loads.
(project / "scripts" / "bash" / "common.sh").unlink()
result = _run_bash(
"create-new-feature-branch.sh", project,
"--json", "--short-name", "x", "X feature",
env_extra={"SPECIFY_INIT_DIR": str(project)},
)
assert result.returncode != 0
assert "requires updated Spec Kit core scripts" in result.stderr
def test_specify_init_dir_with_stale_core_errors(self, tmp_path: Path):
"""With an older core common.sh, a set SPECIFY_INIT_DIR must hard-error
instead of calling the stale get_repo_root that ignores the override."""
project = _setup_project(tmp_path, git=False)
(project / "scripts" / "bash" / "common.sh").write_text(
"#!/usr/bin/env bash\nget_repo_root() { pwd; }\n",
encoding="utf-8",
)
result = _run_bash(
"create-new-feature-branch.sh", project,
"--json", "--short-name", "x", "X feature",
env_extra={"SPECIFY_INIT_DIR": str(tmp_path / "missing")},
)
assert result.returncode != 0
assert "requires updated Spec Kit core scripts" in result.stderr
@pytest.mark.skipif(not HAS_PWSH, reason="pwsh not available")
class TestCreateFeaturePowerShell:
@@ -437,6 +467,43 @@ class TestCreateFeaturePowerShell:
assert "BRANCH_NAME" in data
assert "FEATURE_NUM" in data
def test_specify_init_dir_without_core_errors(self, tmp_path: Path):
"""With no core scripts (only git-common.ps1 loaded), a set SPECIFY_INIT_DIR
hard-errors instead of silently falling back to the walk-up project root."""
project = _setup_project(tmp_path, git=False)
(project / "scripts" / "powershell" / "common.ps1").unlink()
script = project / ".specify" / "extensions" / "git" / "scripts" / "powershell" / "create-new-feature-branch.ps1"
env = {**os.environ, **_GIT_ENV, "SPECIFY_INIT_DIR": str(project)}
result = subprocess.run(
["pwsh", "-NoProfile", "-File", str(script), "-Json", "-ShortName", "x", "X feature"],
cwd=project,
capture_output=True,
text=True,
env=env,
)
assert result.returncode != 0
assert "requires updated Spec Kit core scripts" in result.stderr
def test_specify_init_dir_with_stale_core_errors(self, tmp_path: Path):
"""With an older core common.ps1, a set SPECIFY_INIT_DIR must hard-error
instead of calling the stale Get-RepoRoot that ignores the override."""
project = _setup_project(tmp_path, git=False)
(project / "scripts" / "powershell" / "common.ps1").write_text(
"function Get-RepoRoot { return (Get-Location).Path }\n",
encoding="utf-8",
)
script = project / ".specify" / "extensions" / "git" / "scripts" / "powershell" / "create-new-feature-branch.ps1"
env = {**os.environ, **_GIT_ENV, "SPECIFY_INIT_DIR": str(tmp_path / "missing")}
result = subprocess.run(
["pwsh", "-NoProfile", "-File", str(script), "-Json", "-ShortName", "x", "X feature"],
cwd=project,
capture_output=True,
text=True,
env=env,
)
assert result.returncode != 0
assert "requires updated Spec Kit core scripts" in result.stderr
# ── auto-commit.sh Tests ─────────────────────────────────────────────────────