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
6 changes: 5 additions & 1 deletion scripts/bash/check-prerequisites.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"

# Get feature paths
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
if $PATHS_ONLY; then
_paths_output=$(get_feature_paths --no-persist) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
else
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
fi
Comment on lines +82 to +86
eval "$_paths_output"
unset _paths_output

Expand Down
9 changes: 8 additions & 1 deletion scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ _persist_feature_json() {
}

get_feature_paths() {
local no_persist=false
if [[ "${1:-}" == "--no-persist" ]]; then
no_persist=true
fi
Comment on lines +155 to +158

# Split decl/assignment so a SPECIFY_INIT_DIR validation failure in
# get_repo_root propagates as a hard error instead of being masked by `local`.
local repo_root
Expand All @@ -169,7 +174,9 @@ get_feature_paths() {
# Normalize relative paths to absolute under repo root
[[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
# Persist to feature.json so future sessions without the env var still work
_persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY"
if [[ "$no_persist" == "false" ]]; then
_persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY"
fi
Comment on lines +177 to +179
elif [[ -f "$repo_root/.specify/feature.json" ]]; then
local _fd
_fd=$(read_feature_json_feature_directory "$repo_root")
Expand Down
6 changes: 5 additions & 1 deletion scripts/powershell/check-prerequisites.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ EXAMPLES:
. "$PSScriptRoot/common.ps1"

# Get feature paths
$paths = Get-FeaturePathsEnv
$paths = if ($PathsOnly) {
Get-FeaturePathsEnv -NoPersist
} else {
Get-FeaturePathsEnv
}

# If paths-only mode, output paths and exit (no validation)
if ($PathsOnly) {
Expand Down
8 changes: 7 additions & 1 deletion scripts/powershell/common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ function Save-FeatureJson {
}

function Get-FeaturePathsEnv {
param(
[switch]$NoPersist
)

$repoRoot = Get-RepoRoot
$currentBranch = Get-CurrentBranch

Expand All @@ -158,7 +162,9 @@ function Get-FeaturePathsEnv {
$featureDir = Join-Path $repoRoot $featureDir
}
# Persist to feature.json so future sessions without the env var still work
Save-FeatureJson -RepoRoot $repoRoot -FeatureDirectory $env:SPECIFY_FEATURE_DIRECTORY
if (-not $NoPersist) {
Save-FeatureJson -RepoRoot $repoRoot -FeatureDirectory $env:SPECIFY_FEATURE_DIRECTORY
}
} elseif (Test-Path $featureJson) {
$featureJsonRaw = Get-Content -LiteralPath $featureJson -Raw
try {
Expand Down
105 changes: 98 additions & 7 deletions tests/test_check_prerequisites_paths_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
CHECK_PREREQS_PS = PROJECT_ROOT / "scripts" / "powershell" / "check-prerequisites.ps1"

HAS_PWSH = shutil.which("pwsh") is not None
_WINDOWS_POWERSHELL = (shutil.which("powershell.exe") or shutil.which("powershell")) if os.name == "nt" else None
_WINDOWS_POWERSHELL = (
(shutil.which("powershell.exe") or shutil.which("powershell"))
if os.name == "nt"
else None
)


def _install_bash_scripts(repo: Path) -> None:
Expand Down Expand Up @@ -141,6 +145,41 @@ def test_paths_only_text_mode_on_non_spec_branch(prereq_repo: Path) -> None:
assert "FEATURE_DIR:" in result.stdout


@requires_bash
def test_paths_only_does_not_overwrite_feature_json(prereq_repo: Path) -> None:
"""--paths-only must not rewrite .specify/feature.json when env differs."""
common = (prereq_repo / ".specify" / "scripts" / "bash" / "common.sh").read_text(
encoding="utf-8"
)
script_text = (
prereq_repo / ".specify" / "scripts" / "bash" / "check-prerequisites.sh"
).read_text(encoding="utf-8")
assert "SPECIFY_NO_PERSIST_FEATURE_JSON" not in common
assert "get_feature_paths --no-persist" in script_text
Comment on lines +150 to +158

feat = prereq_repo / "specs" / "001-my-feature"
feat.mkdir(parents=True, exist_ok=True)
_write_feature_json(prereq_repo)
before = (prereq_repo / ".specify" / "feature.json").read_text(encoding="utf-8")
script = prereq_repo / ".specify" / "scripts" / "bash" / "check-prerequisites.sh"
env = _clean_env()
env["SPECIFY_FEATURE_DIRECTORY"] = "specs/999-temp"
result = subprocess.run(
["bash", str(script), "--json", "--paths-only"],
cwd=prereq_repo,
capture_output=True,
text=True,
check=False,
env=env,
)
assert result.returncode == 0, result.stderr
assert (prereq_repo / ".specify" / "feature.json").read_text(
encoding="utf-8"
) == before
data = json.loads(result.stdout)
assert data["FEATURE_DIR"].endswith("specs/999-temp")


@requires_bash
def test_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
"""Without --paths-only, feature directory validation must still fail on main."""
Expand All @@ -160,13 +199,17 @@ def test_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
# ── PowerShell tests ──────────────────────────────────────────────────────


@pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available")
@pytest.mark.skipif(
not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available"
)
def test_ps_paths_only_succeeds_on_non_spec_branch(prereq_repo: Path) -> None:
"""-PathsOnly must return paths when feature.json pins the feature dir."""
feat = prereq_repo / "specs" / "001-my-feature"
feat.mkdir(parents=True, exist_ok=True)
_write_feature_json(prereq_repo)
script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
script = (
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
)
exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL
result = subprocess.run(
[exe, "-NoProfile", "-File", str(script), "-Json", "-PathsOnly"],
Expand All @@ -183,7 +226,9 @@ def test_ps_paths_only_succeeds_on_non_spec_branch(prereq_repo: Path) -> None:
assert "FEATURE_DIR" in data


@pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available")
@pytest.mark.skipif(
not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available"
)
def test_ps_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None:
"""-PathsOnly must also work when feature.json and SPECIFY_FEATURE agree."""
subprocess.run(
Expand All @@ -194,7 +239,9 @@ def test_ps_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None:
feat = prereq_repo / "specs" / "001-my-feature"
feat.mkdir(parents=True, exist_ok=True)
_write_feature_json(prereq_repo)
script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
script = (
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
)
exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL
env = _clean_env()
env["SPECIFY_FEATURE"] = "001-my-feature"
Expand All @@ -211,10 +258,54 @@ def test_ps_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None:
assert "FEATURE_DIR" in data


@pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available")
@pytest.mark.skipif(
not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available"
)
def test_ps_paths_only_does_not_overwrite_feature_json(prereq_repo: Path) -> None:
"""-PathsOnly must not rewrite .specify/feature.json when env differs."""
common = (
prereq_repo / ".specify" / "scripts" / "powershell" / "common.ps1"
).read_text(encoding="utf-8")
script_text = (
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
).read_text(encoding="utf-8")
assert "SPECIFY_NO_PERSIST_FEATURE_JSON" not in common
assert "Get-FeaturePathsEnv -NoPersist" in script_text

feat = prereq_repo / "specs" / "001-my-feature"
feat.mkdir(parents=True, exist_ok=True)
_write_feature_json(prereq_repo)
before = (prereq_repo / ".specify" / "feature.json").read_text(encoding="utf-8")
script = (
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
)
exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL
env = _clean_env()
env["SPECIFY_FEATURE_DIRECTORY"] = "specs/999-temp"
result = subprocess.run(
[exe, "-NoProfile", "-File", str(script), "-Json", "-PathsOnly"],
cwd=prereq_repo,
capture_output=True,
text=True,
check=False,
env=env,
)
assert result.returncode == 0, result.stderr
assert (prereq_repo / ".specify" / "feature.json").read_text(
encoding="utf-8"
) == before
data = json.loads(result.stdout)
assert data["FEATURE_DIR"].replace("\\", "/").endswith("specs/999-temp")


@pytest.mark.skipif(
not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available"
)
def test_ps_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
"""Without -PathsOnly, feature directory validation must still fail on main."""
script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
script = (
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
)
exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL
result = subprocess.run(
[exe, "-NoProfile", "-File", str(script), "-Json"],
Expand Down