fix(shared-infra): remove stale managed scripts the core no longer ships (#3076)#3098
fix(shared-infra): remove stale managed scripts the core no longer ships (#3076)#3098montfort wants to merge 5 commits into
Conversation
…ips (github#3076) install_shared_infra never removed shared scripts a prior (pre-refactor) install recorded but the current core no longer ships — e.g. the legacy scripts/<variant>/update-agent-context.sh, superseded by the bundled agent-context extension. On a legacy project the orphan lingers and crashes when it sources a refreshed common.sh (HAS_GIT unbound under set -u). Apply the stale-removal that integration_upgrade already performs to install_shared_infra: manifest-tracked scripts the current bundle no longer produces are removed, but only managed copies (hash matches the manifest); user-customized files, symlinks, and recovered entries are preserved. Guarded so a missing/empty source can't trigger mass deletion, and the safe-destination check prevents unlinking through a symlinked ancestor. Add IntegrationManifest.remove(); drop the stale update-agent-context.sh reference in CONTRIBUTING.md. AI assistance: implemented with Claude Code (Anthropic); reviewed and validated locally (ruff clean, full suite 4176 passed, manual CLI repro). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates Spec Kit’s shared infrastructure installer to proactively remove manifest-tracked legacy scripts that the core no longer ships (addressing #3076), preventing orphaned scripts from breaking after refactors (e.g., old update-agent-context.sh sourcing a newer common.sh).
Changes:
- Track which shared script/template paths the current bundle produces and delete obsolete managed scripts from prior installs during
install_shared_infra. - Add
IntegrationManifest.remove()to keep manifests consistent when callers delete tracked files. - Add regression tests for stale-script deletion vs. preserving user-modified stale scripts; update
CONTRIBUTING.mdto remove the stale script reference.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/specify_cli/shared_infra.py |
Implements stale managed-script cleanup during shared infra install, with safety checks and manifest updates. |
src/specify_cli/integrations/manifest.py |
Adds IntegrationManifest.remove() helper to drop tracked entries without reaching into internals. |
tests/integrations/test_cli.py |
Adds unit tests covering stale managed-script removal and preservation when modified. |
CONTRIBUTING.md |
Removes mention of legacy update-agent-context.sh as a transitive dependency of common.sh. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…phan manifest) - Set scripts_scanned only after a real source file is seen, so an empty variant source can't trigger mass deletion of tracked scripts. - Prune a stale manifest entry even when its file is already gone from disk, keeping the manifest consistent (previously left tracked forever). - Add a test for each edge case. Addresses the Copilot review comments on github#3098. AI assistance: Claude Code (Anthropic), reviewed/validated locally (ruff clean, full suite 4178 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the review — both points addressed in ffca99c:
Full suite green (4178 passed), ruff clean. |
- Skip absolute / '..' manifest keys before any filesystem access in stale-cleanup, so a corrupted/hand-edited manifest can't make it touch paths outside the project root (mirrors IntegrationManifest.check_modified / uninstall). - Clarify the scripts_scanned comment: the safety hinge is that flag, not seen_rels (which also holds template paths). - Add a containment test: a traversal manifest key is skipped, its target untouched. Addresses the second round of Copilot review on github#3098. AI assistance: Claude Code (Anthropic); validated locally (ruff clean, full suite 4179 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks — second round addressed in the latest push:
Full suite green (4179 passed), ruff clean. |
| def remove(self, rel_path: str | Path) -> bool: | ||
| """Drop *rel_path* from the tracked file set and any recovered marker. | ||
|
|
||
| Operates purely on the manifest's recorded key; it does NOT touch the | ||
| file on disk. Returns ``True`` if an entry was present and removed. | ||
| Used to keep the manifest consistent after a caller deletes a stale | ||
| managed file that the current install no longer ships. | ||
| """ | ||
| normalized = Path(rel_path).as_posix() | ||
| self._recovered_files.discard(normalized) | ||
| return self._files.pop(normalized, None) is not None |
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback
…s (review) IntegrationManifest.remove() now applies the same lexical validation and normalization as record_existing() / is_recovered(): absolute paths and '..' segments are rejected (return False) instead of being used verbatim as a key. Keeps the manifest API consistent. Adds tests (valid drop + no-op, absolute rejected, traversal rejected). Addresses the third round of Copilot review on github#3098. AI assistance: Claude Code (Anthropic); validated locally (ruff clean, full suite 4182 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @mnriem — all of Copilot's feedback is now addressed across the three rounds:
Each fix has a regression test. Full suite green (4182 passed), ruff clean. Ready for re-review whenever you have a moment — thanks for the careful look. |
| # Guard corrupted/hand-edited manifest keys BEFORE any filesystem | ||
| # access: an absolute path or a ``..`` segment could point outside | ||
| # the project root. Mirrors the containment checks in | ||
| # IntegrationManifest.check_modified / uninstall. | ||
| rel_path = Path(rel) | ||
| if rel_path.is_absolute() or ".." in rel_path.parts: | ||
| continue |
There was a problem hiding this comment.
Good catch — addressed in fecb5e3.
The guard now runs the canonical containment helper after the fast lexical reject: _validate_rel_path(rel_path, project_path) (the same one IntegrationManifest.is_recovered / remove use) resolves the join and confirms it stays within the project root. A Windows drive-relative key like C:tmp\file — not is_absolute(), but discarding the root when joined — now fails containment and the key is continue-skipped before any stat/unlink. And because the check is wrapped so a ValueError skips the key instead of propagating, a corrupted manifest no longer turns into an install-time hard failure.
Added a regression test (test_shared_infra_stale_cleanup_skips_escaping_key_without_failing) that forces _validate_rel_path to reject a managed key — portably simulating the drive-relative escape, since real drive-relative paths only escape on Windows — and asserts the install skips it without raising while still installing the real scripts. Full suite green (4183 passed, 3 skipped; ruff clean).
|
Please address Copilot feedback |
…ust lexically (review) The stale-script cleanup guarded manifest keys with a lexical check only (is_absolute() / ".." segments). On Windows a drive-relative key such as "C:tmp\\file" is not is_absolute(), yet joining it onto the project path discards the root — so cleanup could stat/unlink outside the project before _ensure_safe_shared_destination raised, and a corrupted manifest key turned into an install-time hard failure (ValueError) instead of being skipped. Reuse the canonical containment helper (_validate_rel_path, the same one IntegrationManifest.is_recovered / remove use): after the fast lexical reject, resolve the join and confirm it stays within the project root; a key that still escapes is skipped, never unlinked, never fatal. Adds a regression test that forces _validate_rel_path to reject a managed key (portably simulating the Windows drive-relative escape) and asserts the install skips it without failing and still installs the real scripts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Description
Fixes #3076.
install_shared_infranever removed shared scripts that a prior (pre-refactor) install recorded but the current core no longer ships — e.g. the legacyscripts/<variant>/update-agent-context.sh, superseded by the bundledagent-contextextension. On a legacy project the orphan lingers, and because a refreshedcommon.shno longer exportsHAS_GIT, running the old script crashes (HAS_GIT: unbound variableunderset -u). As discussed in the issue, the right fix is stale-file cleanup on (re)install, not re-introducingHAS_GIT/branch handling into a deleted script.This applies the same stale-removal that
integration_upgradealready performs (_migrate_commands.py) toinstall_shared_infra: after the bundle is processed, any manifest-tracked script under.specify/scripts/<variant>/that the current bundle no longer produces is removed. Safety:_is_managed). A user-customized file (hash diverges), a symlink, or arecoveredentry is preserved.scripts_scanned— if the script source is missing/empty (ordest_variantis symlinked and skipped), the cleanup does not run, so it can never trigger mass deletion.unlink, so a symlinked ancestor can't let a delete escape the project root.Also drops the stale
update-agent-context.shreference inCONTRIBUTING.md(it is no longer sourced bycommon.sh), and adds a smallIntegrationManifest.remove()so the manifest stays consistent without mutating its internals from outside.Scoped to the script variant the bug is about; templates are untouched. PowerShell legacy orphans are covered by the same code path (it iterates the active variant).
Testing
uv sync --extra test && .venv/bin/python -m pytest tests -q→ 4176 passed, 3 skipped (ruff clean)tests/integrations/test_cli.py): stale managed script removed + manifest stops tracking it; user-modified stale preserved (hash-safe)Manual test results
Agent: Claude Code · OS/Shell: Linux / bash
specify init --here --force --integration claude --script sh(with an injected manifest-tracked legacyupdate-agent-context.sh)⚠ Removed 1 obsolete shared script(s): .specify/scripts/bash/update-agent-context.sh; orphan gone from disk and manifest;common.shand the other shipped scripts preservedAI Disclosure
I used Claude Code (Anthropic) to explore the codebase, implement the fix following the existing
integration_upgradestale-cleanup pattern, and write the tests. All changes were reviewed and validated locally by me before submission.Note: I left the
CHANGELOG.mdentry out — entries carry the merged PR number and the<!-- insert new changelog below this comment -->marker suggests your release flow manages insertion. Happy to add one if you'd prefer it in the PR.