Skip to content

Commit 3227b96

Browse files
authored
fix: paths-only skips branch validation, setup-plan preserves existing plan (#2672)
* fix: paths-only skips branch validation, setup-plan preserves existing plan (#2653) - check-prerequisites.sh/ps1: move branch validation after --paths-only early exit so --paths-only returns paths without requiring a spec branch - setup-plan.sh/ps1: skip template copy when plan.md already exists to prevent overwriting user-authored plans on reruns - setup-plan.sh: send status messages to stderr in --json mode so stdout remains parseable JSON - Add tests for both fixes (bash + PowerShell) * fix: remove trailing whitespace in PowerShell scripts * fix: route PS skip message to stderr in -Json mode, add PS JSON assertions Address review: setup-plan.ps1 Write-Output polluted stdout in -Json mode when plan.md already existed. Use [Console]::Error.WriteLine() when -Json is set. Add json.loads + stderr assertions to the PS rerun test to catch regressions. * fix: use Test-Path -PathType Leaf for plan existence check Bare Test-Path matches directories too, which would silently skip plan creation if a directory existed at the plan.md path.
1 parent d116ce2 commit 3227b96

6 files changed

Lines changed: 475 additions & 27 deletions

File tree

scripts/bash/check-prerequisites.sh

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,12 @@ done
7878
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7979
source "$SCRIPT_DIR/common.sh"
8080

81-
# Get feature paths and validate branch
81+
# Get feature paths
8282
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
8383
eval "$_paths_output"
8484
unset _paths_output
85-
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
8685

87-
# If paths-only mode, output paths and exit (support JSON + paths-only combined)
86+
# If paths-only mode, output paths and exit (no validation)
8887
if $PATHS_ONLY; then
8988
if $JSON_MODE; then
9089
# Minimal JSON paths payload (no validation performed)
@@ -112,6 +111,9 @@ if $PATHS_ONLY; then
112111
exit 0
113112
fi
114113

114+
# Validate branch name
115+
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
116+
115117
# Validate required directories and files
116118
if [[ ! -d "$FEATURE_DIR" ]]; then
117119
echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2

scripts/bash/setup-plan.sh

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,31 @@ fi
4040
# Ensure the feature directory exists
4141
mkdir -p "$FEATURE_DIR"
4242

43-
# Copy plan template if it exists
44-
TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT") || true
45-
if [[ -n "$TEMPLATE" ]] && [[ -f "$TEMPLATE" ]]; then
46-
cp "$TEMPLATE" "$IMPL_PLAN"
47-
echo "Copied plan template to $IMPL_PLAN"
43+
# Copy plan template if plan doesn't already exist
44+
if [[ -f "$IMPL_PLAN" ]]; then
45+
if $JSON_MODE; then
46+
echo "Plan already exists at $IMPL_PLAN, skipping template copy" >&2
47+
else
48+
echo "Plan already exists at $IMPL_PLAN, skipping template copy"
49+
fi
4850
else
49-
echo "Warning: Plan template not found"
50-
# Create a basic plan file if template doesn't exist
51-
touch "$IMPL_PLAN"
51+
TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT") || true
52+
if [[ -n "$TEMPLATE" ]] && [[ -f "$TEMPLATE" ]]; then
53+
cp "$TEMPLATE" "$IMPL_PLAN"
54+
if $JSON_MODE; then
55+
echo "Copied plan template to $IMPL_PLAN" >&2
56+
else
57+
echo "Copied plan template to $IMPL_PLAN"
58+
fi
59+
else
60+
if $JSON_MODE; then
61+
echo "Warning: Plan template not found" >&2
62+
else
63+
echo "Warning: Plan template not found"
64+
fi
65+
# Create a basic plan file if template doesn't exist
66+
touch "$IMPL_PLAN"
67+
fi
5268
fi
5369

5470
# Output results

scripts/powershell/check-prerequisites.ps1

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,10 @@ EXAMPLES:
5656
# Source common functions
5757
. "$PSScriptRoot/common.ps1"
5858

59-
# Get feature paths and validate branch
59+
# Get feature paths
6060
$paths = Get-FeaturePathsEnv
6161

62-
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit:$paths.HAS_GIT)) {
63-
exit 1
64-
}
65-
66-
# If paths-only mode, output paths and exit (support combined -Json -PathsOnly)
62+
# If paths-only mode, output paths and exit (no validation)
6763
if ($PathsOnly) {
6864
if ($Json) {
6965
[PSCustomObject]@{
@@ -85,6 +81,11 @@ if ($PathsOnly) {
8581
exit 0
8682
}
8783

84+
# Validate branch name
85+
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit:$paths.HAS_GIT)) {
86+
exit 1
87+
}
88+
8889
# Validate required directories and files
8990
if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) {
9091
Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)"

scripts/powershell/setup-plan.ps1

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,25 @@ if (-not (Test-FeatureJsonMatchesFeatureDir -RepoRoot $paths.REPO_ROOT -ActiveFe
3333
# Ensure the feature directory exists
3434
New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null
3535

36-
# Copy plan template if it exists, otherwise note it or create empty file
37-
$template = Resolve-Template -TemplateName 'plan-template' -RepoRoot $paths.REPO_ROOT
38-
if ($template -and (Test-Path $template)) {
39-
# Read the template content and write it to the implementation plan file with UTF-8 encoding without BOM
40-
$content = [System.IO.File]::ReadAllText($template)
41-
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
42-
[System.IO.File]::WriteAllText($paths.IMPL_PLAN, $content, $utf8NoBom)
36+
# Copy plan template if plan doesn't already exist
37+
if (Test-Path $paths.IMPL_PLAN -PathType Leaf) {
38+
if ($Json) {
39+
[Console]::Error.WriteLine("Plan already exists at $($paths.IMPL_PLAN), skipping template copy")
40+
} else {
41+
Write-Output "Plan already exists at $($paths.IMPL_PLAN), skipping template copy"
42+
}
4343
} else {
44-
Write-Warning "Plan template not found"
45-
# Create a basic plan file if template doesn't exist
46-
New-Item -ItemType File -Path $paths.IMPL_PLAN -Force | Out-Null
44+
$template = Resolve-Template -TemplateName 'plan-template' -RepoRoot $paths.REPO_ROOT
45+
if ($template -and (Test-Path $template)) {
46+
# Read the template content and write it to the implementation plan file with UTF-8 encoding without BOM
47+
$content = [System.IO.File]::ReadAllText($template)
48+
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
49+
[System.IO.File]::WriteAllText($paths.IMPL_PLAN, $content, $utf8NoBom)
50+
} else {
51+
Write-Warning "Plan template not found"
52+
# Create a basic plan file if template doesn't exist
53+
New-Item -ItemType File -Path $paths.IMPL_PLAN -Force | Out-Null
54+
}
4755
}
4856

4957
# Output results
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""Tests for check-prerequisites --paths-only skipping branch validation (#2653)."""
2+
3+
import json
4+
import os
5+
import shutil
6+
import subprocess
7+
from pathlib import Path
8+
9+
import pytest
10+
11+
from tests.conftest import requires_bash
12+
13+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
14+
COMMON_SH = PROJECT_ROOT / "scripts" / "bash" / "common.sh"
15+
CHECK_PREREQS_SH = PROJECT_ROOT / "scripts" / "bash" / "check-prerequisites.sh"
16+
COMMON_PS = PROJECT_ROOT / "scripts" / "powershell" / "common.ps1"
17+
CHECK_PREREQS_PS = PROJECT_ROOT / "scripts" / "powershell" / "check-prerequisites.ps1"
18+
19+
HAS_PWSH = shutil.which("pwsh") is not None
20+
_POWERSHELL = shutil.which("powershell.exe") or shutil.which("powershell")
21+
22+
23+
def _install_bash_scripts(repo: Path) -> None:
24+
d = repo / ".specify" / "scripts" / "bash"
25+
d.mkdir(parents=True, exist_ok=True)
26+
shutil.copy(COMMON_SH, d / "common.sh")
27+
shutil.copy(CHECK_PREREQS_SH, d / "check-prerequisites.sh")
28+
29+
30+
def _install_ps_scripts(repo: Path) -> None:
31+
d = repo / ".specify" / "scripts" / "powershell"
32+
d.mkdir(parents=True, exist_ok=True)
33+
shutil.copy(COMMON_PS, d / "common.ps1")
34+
shutil.copy(CHECK_PREREQS_PS, d / "check-prerequisites.ps1")
35+
36+
37+
def _clean_env() -> dict[str, str]:
38+
env = os.environ.copy()
39+
for key in list(env):
40+
if key.startswith("SPECIFY_"):
41+
env.pop(key)
42+
return env
43+
44+
45+
def _git_init(repo: Path) -> None:
46+
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
47+
subprocess.run(
48+
["git", "config", "user.email", "test@example.com"], cwd=repo, check=True
49+
)
50+
subprocess.run(["git", "config", "user.name", "Test User"], cwd=repo, check=True)
51+
subprocess.run(
52+
["git", "commit", "--allow-empty", "-m", "init", "-q"], cwd=repo, check=True
53+
)
54+
55+
56+
@pytest.fixture
57+
def prereq_repo(tmp_path: Path) -> Path:
58+
repo = tmp_path / "proj"
59+
repo.mkdir()
60+
_git_init(repo)
61+
(repo / ".specify").mkdir()
62+
_install_bash_scripts(repo)
63+
_install_ps_scripts(repo)
64+
return repo
65+
66+
67+
# ── Bash tests ────────────────────────────────────────────────────────────
68+
69+
70+
@requires_bash
71+
def test_paths_only_succeeds_on_non_spec_branch(prereq_repo: Path) -> None:
72+
"""--paths-only must return paths without branch validation (main branch)."""
73+
script = prereq_repo / ".specify" / "scripts" / "bash" / "check-prerequisites.sh"
74+
result = subprocess.run(
75+
["bash", str(script), "--json", "--paths-only"],
76+
cwd=prereq_repo,
77+
capture_output=True,
78+
text=True,
79+
check=False,
80+
env=_clean_env(),
81+
)
82+
assert result.returncode == 0, result.stderr
83+
data = json.loads(result.stdout)
84+
assert "REPO_ROOT" in data
85+
assert "BRANCH" in data
86+
assert "FEATURE_DIR" in data
87+
88+
89+
@requires_bash
90+
def test_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None:
91+
"""--paths-only must also work on a properly named spec branch."""
92+
subprocess.run(
93+
["git", "checkout", "-q", "-b", "001-my-feature"],
94+
cwd=prereq_repo,
95+
check=True,
96+
)
97+
script = prereq_repo / ".specify" / "scripts" / "bash" / "check-prerequisites.sh"
98+
result = subprocess.run(
99+
["bash", str(script), "--json", "--paths-only"],
100+
cwd=prereq_repo,
101+
capture_output=True,
102+
text=True,
103+
check=False,
104+
env=_clean_env(),
105+
)
106+
assert result.returncode == 0, result.stderr
107+
data = json.loads(result.stdout)
108+
assert "FEATURE_DIR" in data
109+
assert "001-my-feature" in data.get("BRANCH", "")
110+
111+
112+
@requires_bash
113+
def test_paths_only_text_mode_on_non_spec_branch(prereq_repo: Path) -> None:
114+
"""--paths-only without --json must return text paths on a non-spec branch."""
115+
script = prereq_repo / ".specify" / "scripts" / "bash" / "check-prerequisites.sh"
116+
result = subprocess.run(
117+
["bash", str(script), "--paths-only"],
118+
cwd=prereq_repo,
119+
capture_output=True,
120+
text=True,
121+
check=False,
122+
env=_clean_env(),
123+
)
124+
assert result.returncode == 0, result.stderr
125+
assert "REPO_ROOT:" in result.stdout
126+
assert "FEATURE_DIR:" in result.stdout
127+
128+
129+
@requires_bash
130+
def test_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
131+
"""Without --paths-only, branch validation must still fail on main."""
132+
script = prereq_repo / ".specify" / "scripts" / "bash" / "check-prerequisites.sh"
133+
result = subprocess.run(
134+
["bash", str(script), "--json"],
135+
cwd=prereq_repo,
136+
capture_output=True,
137+
text=True,
138+
check=False,
139+
env=_clean_env(),
140+
)
141+
assert result.returncode != 0
142+
assert "Not on a feature branch" in result.stderr
143+
144+
145+
# ── PowerShell tests ──────────────────────────────────────────────────────
146+
147+
148+
@pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available")
149+
def test_ps_paths_only_succeeds_on_non_spec_branch(prereq_repo: Path) -> None:
150+
"""-PathsOnly must return paths without branch validation (main branch)."""
151+
script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
152+
exe = "pwsh" if HAS_PWSH else _POWERSHELL
153+
result = subprocess.run(
154+
[exe, "-NoProfile", "-File", str(script), "-Json", "-PathsOnly"],
155+
cwd=prereq_repo,
156+
capture_output=True,
157+
text=True,
158+
check=False,
159+
env=_clean_env(),
160+
)
161+
assert result.returncode == 0, result.stderr
162+
data = json.loads(result.stdout)
163+
assert "REPO_ROOT" in data
164+
assert "BRANCH" in data
165+
assert "FEATURE_DIR" in data
166+
167+
168+
@pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available")
169+
def test_ps_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None:
170+
"""-PathsOnly must also work on a properly named spec branch."""
171+
subprocess.run(
172+
["git", "checkout", "-q", "-b", "001-my-feature"],
173+
cwd=prereq_repo,
174+
check=True,
175+
)
176+
script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
177+
exe = "pwsh" if HAS_PWSH else _POWERSHELL
178+
result = subprocess.run(
179+
[exe, "-NoProfile", "-File", str(script), "-Json", "-PathsOnly"],
180+
cwd=prereq_repo,
181+
capture_output=True,
182+
text=True,
183+
check=False,
184+
env=_clean_env(),
185+
)
186+
assert result.returncode == 0, result.stderr
187+
data = json.loads(result.stdout)
188+
assert "FEATURE_DIR" in data
189+
190+
191+
@pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available")
192+
def test_ps_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
193+
"""Without -PathsOnly, branch validation must still fail on main."""
194+
script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
195+
exe = "pwsh" if HAS_PWSH else _POWERSHELL
196+
result = subprocess.run(
197+
[exe, "-NoProfile", "-File", str(script), "-Json"],
198+
cwd=prereq_repo,
199+
capture_output=True,
200+
text=True,
201+
check=False,
202+
env=_clean_env(),
203+
)
204+
assert result.returncode != 0
205+
assert "Not on a feature branch" in result.stderr

0 commit comments

Comments
 (0)