Skip to content

Commit 072c69d

Browse files
committed
fix: avoid paths-only feature context writes
1 parent 2dd1ca4 commit 072c69d

5 files changed

Lines changed: 123 additions & 11 deletions

File tree

scripts/bash/check-prerequisites.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7979
source "$SCRIPT_DIR/common.sh"
8080

8181
# Get feature paths
82-
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
82+
if $PATHS_ONLY; then
83+
_paths_output=$(get_feature_paths --no-persist) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
84+
else
85+
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
86+
fi
8387
eval "$_paths_output"
8488
unset _paths_output
8589

scripts/bash/common.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ _persist_feature_json() {
119119
}
120120

121121
get_feature_paths() {
122+
local no_persist=false
123+
if [[ "${1:-}" == "--no-persist" ]]; then
124+
no_persist=true
125+
fi
126+
122127
local repo_root=$(get_repo_root)
123128
local current_branch=$(get_current_branch)
124129

@@ -132,7 +137,9 @@ get_feature_paths() {
132137
# Normalize relative paths to absolute under repo root
133138
[[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
134139
# Persist to feature.json so future sessions without the env var still work
135-
_persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY"
140+
if [[ "$no_persist" == "false" ]]; then
141+
_persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY"
142+
fi
136143
elif [[ -f "$repo_root/.specify/feature.json" ]]; then
137144
local _fd
138145
_fd=$(read_feature_json_feature_directory "$repo_root")

scripts/powershell/check-prerequisites.ps1

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ EXAMPLES:
5757
. "$PSScriptRoot/common.ps1"
5858

5959
# Get feature paths
60-
$paths = Get-FeaturePathsEnv
60+
$paths = if ($PathsOnly) {
61+
Get-FeaturePathsEnv -NoPersist
62+
} else {
63+
Get-FeaturePathsEnv
64+
}
6165

6266
# If paths-only mode, output paths and exit (no validation)
6367
if ($PathsOnly) {

scripts/powershell/common.ps1

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ function Save-FeatureJson {
101101
}
102102

103103
function Get-FeaturePathsEnv {
104+
param(
105+
[switch]$NoPersist
106+
)
107+
104108
$repoRoot = Get-RepoRoot
105109
$currentBranch = Get-CurrentBranch
106110

@@ -116,7 +120,9 @@ function Get-FeaturePathsEnv {
116120
$featureDir = Join-Path $repoRoot $featureDir
117121
}
118122
# Persist to feature.json so future sessions without the env var still work
119-
Save-FeatureJson -RepoRoot $repoRoot -FeatureDirectory $env:SPECIFY_FEATURE_DIRECTORY
123+
if (-not $NoPersist) {
124+
Save-FeatureJson -RepoRoot $repoRoot -FeatureDirectory $env:SPECIFY_FEATURE_DIRECTORY
125+
}
120126
} elseif (Test-Path $featureJson) {
121127
$featureJsonRaw = Get-Content -LiteralPath $featureJson -Raw
122128
try {

tests/test_check_prerequisites_paths_only.py

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
CHECK_PREREQS_PS = PROJECT_ROOT / "scripts" / "powershell" / "check-prerequisites.ps1"
1818

1919
HAS_PWSH = shutil.which("pwsh") is not None
20-
_WINDOWS_POWERSHELL = (shutil.which("powershell.exe") or shutil.which("powershell")) if os.name == "nt" else None
20+
_WINDOWS_POWERSHELL = (
21+
(shutil.which("powershell.exe") or shutil.which("powershell"))
22+
if os.name == "nt"
23+
else None
24+
)
2125

2226

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

143147

148+
@requires_bash
149+
def test_paths_only_does_not_overwrite_feature_json(prereq_repo: Path) -> None:
150+
"""--paths-only must not rewrite .specify/feature.json when env differs."""
151+
common = (prereq_repo / ".specify" / "scripts" / "bash" / "common.sh").read_text(
152+
encoding="utf-8"
153+
)
154+
script_text = (
155+
prereq_repo / ".specify" / "scripts" / "bash" / "check-prerequisites.sh"
156+
).read_text(encoding="utf-8")
157+
assert "SPECIFY_NO_PERSIST_FEATURE_JSON" not in common
158+
assert "get_feature_paths --no-persist" in script_text
159+
160+
feat = prereq_repo / "specs" / "001-my-feature"
161+
feat.mkdir(parents=True, exist_ok=True)
162+
_write_feature_json(prereq_repo)
163+
before = (prereq_repo / ".specify" / "feature.json").read_text(encoding="utf-8")
164+
script = prereq_repo / ".specify" / "scripts" / "bash" / "check-prerequisites.sh"
165+
env = _clean_env()
166+
env["SPECIFY_FEATURE_DIRECTORY"] = "specs/999-temp"
167+
result = subprocess.run(
168+
["bash", str(script), "--json", "--paths-only"],
169+
cwd=prereq_repo,
170+
capture_output=True,
171+
text=True,
172+
check=False,
173+
env=env,
174+
)
175+
assert result.returncode == 0, result.stderr
176+
assert (prereq_repo / ".specify" / "feature.json").read_text(
177+
encoding="utf-8"
178+
) == before
179+
data = json.loads(result.stdout)
180+
assert data["FEATURE_DIR"].endswith("specs/999-temp")
181+
182+
144183
@requires_bash
145184
def test_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
146185
"""Without --paths-only, feature directory validation must still fail on main."""
@@ -160,13 +199,17 @@ def test_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
160199
# ── PowerShell tests ──────────────────────────────────────────────────────
161200

162201

163-
@pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available")
202+
@pytest.mark.skipif(
203+
not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available"
204+
)
164205
def test_ps_paths_only_succeeds_on_non_spec_branch(prereq_repo: Path) -> None:
165206
"""-PathsOnly must return paths when feature.json pins the feature dir."""
166207
feat = prereq_repo / "specs" / "001-my-feature"
167208
feat.mkdir(parents=True, exist_ok=True)
168209
_write_feature_json(prereq_repo)
169-
script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
210+
script = (
211+
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
212+
)
170213
exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL
171214
result = subprocess.run(
172215
[exe, "-NoProfile", "-File", str(script), "-Json", "-PathsOnly"],
@@ -183,7 +226,9 @@ def test_ps_paths_only_succeeds_on_non_spec_branch(prereq_repo: Path) -> None:
183226
assert "FEATURE_DIR" in data
184227

185228

186-
@pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available")
229+
@pytest.mark.skipif(
230+
not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available"
231+
)
187232
def test_ps_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None:
188233
"""-PathsOnly must also work when feature.json and SPECIFY_FEATURE agree."""
189234
subprocess.run(
@@ -194,7 +239,9 @@ def test_ps_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None:
194239
feat = prereq_repo / "specs" / "001-my-feature"
195240
feat.mkdir(parents=True, exist_ok=True)
196241
_write_feature_json(prereq_repo)
197-
script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
242+
script = (
243+
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
244+
)
198245
exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL
199246
env = _clean_env()
200247
env["SPECIFY_FEATURE"] = "001-my-feature"
@@ -211,10 +258,54 @@ def test_ps_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None:
211258
assert "FEATURE_DIR" in data
212259

213260

214-
@pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available")
261+
@pytest.mark.skipif(
262+
not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available"
263+
)
264+
def test_ps_paths_only_does_not_overwrite_feature_json(prereq_repo: Path) -> None:
265+
"""-PathsOnly must not rewrite .specify/feature.json when env differs."""
266+
common = (
267+
prereq_repo / ".specify" / "scripts" / "powershell" / "common.ps1"
268+
).read_text(encoding="utf-8")
269+
script_text = (
270+
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
271+
).read_text(encoding="utf-8")
272+
assert "SPECIFY_NO_PERSIST_FEATURE_JSON" not in common
273+
assert "Get-FeaturePathsEnv -NoPersist" in script_text
274+
275+
feat = prereq_repo / "specs" / "001-my-feature"
276+
feat.mkdir(parents=True, exist_ok=True)
277+
_write_feature_json(prereq_repo)
278+
before = (prereq_repo / ".specify" / "feature.json").read_text(encoding="utf-8")
279+
script = (
280+
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
281+
)
282+
exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL
283+
env = _clean_env()
284+
env["SPECIFY_FEATURE_DIRECTORY"] = "specs/999-temp"
285+
result = subprocess.run(
286+
[exe, "-NoProfile", "-File", str(script), "-Json", "-PathsOnly"],
287+
cwd=prereq_repo,
288+
capture_output=True,
289+
text=True,
290+
check=False,
291+
env=env,
292+
)
293+
assert result.returncode == 0, result.stderr
294+
assert (prereq_repo / ".specify" / "feature.json").read_text(
295+
encoding="utf-8"
296+
) == before
297+
data = json.loads(result.stdout)
298+
assert data["FEATURE_DIR"].replace("\\", "/").endswith("specs/999-temp")
299+
300+
301+
@pytest.mark.skipif(
302+
not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available"
303+
)
215304
def test_ps_normal_mode_still_validates_branch(prereq_repo: Path) -> None:
216305
"""Without -PathsOnly, feature directory validation must still fail on main."""
217-
script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
306+
script = (
307+
prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1"
308+
)
218309
exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL
219310
result = subprocess.run(
220311
[exe, "-NoProfile", "-File", str(script), "-Json"],

0 commit comments

Comments
 (0)