fix: --force now overwrites shared infra files during init and upgrade#2320
Merged
mnriem merged 2 commits intogithub:mainfrom Apr 22, 2026
Merged
fix: --force now overwrites shared infra files during init and upgrade#2320mnriem merged 2 commits intogithub:mainfrom
--force now overwrites shared infra files during init and upgrade#2320mnriem merged 2 commits intogithub:mainfrom
Conversation
_install_shared_infra() previously skipped all existing files under .specify/scripts/ and .specify/templates/, regardless of --force. This meant users could never receive upstream fixes to shared scripts or templates after initial project setup. Changes: - Add force parameter to _install_shared_infra(); when True, existing files are overwritten with the latest bundled versions - Wire force=True through specify init --here --force and specify integration upgrade --force call sites - Replace hidden logging.warning with visible console output listing skipped files and suggesting --force - Fix contradictory upgrade docs that claimed --force updated shared infra (it didn't) and warned about overwrites (they didn't happen) - Add 6 tests: unit tests for skip/overwrite/warning behavior, plus end-to-end CLI tests for both --force and non-force paths Fixes github#2319
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes the long-standing behavior where shared infrastructure under .specify/scripts/ and .specify/templates/ was never overwritten, even when users ran specify init --here --force or specify integration upgrade --force, preventing projects from receiving upstream updates to shared scripts/templates.
Changes:
- Add a
force: bool = Falseparameter to_install_shared_infra()to allow overwriting existing shared infra files when explicitly requested. - Wire
forcethroughspecify initandspecify integration upgradeso--forceactually refreshes shared infra. - Update upgrade documentation and add tests covering skip/overwrite behavior and the emitted warning output.
Show a summary per file
| File | Description |
|---|---|
tests/integrations/test_cli.py |
Adds unit + E2E coverage for shared infra skip/overwrite behavior and warning output. |
src/specify_cli/__init__.py |
Implements force overwrite logic in _install_shared_infra(), updates CLI call sites, and prints a visible skip warning. |
docs/upgrade.md |
Aligns upgrade docs with actual behavior (shared infra only overwritten when --force is used). |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 3/3 changed files
- Comments generated: 1
Address review feedback: the generic '--force' suggestion was misleading when _install_shared_infra is called from integration install/switch (which don't have a --force for shared infra). Now points users to the specific commands that can refresh shared infra: 'specify init --here --force' or 'specify integration upgrade --force'.
Contributor
There was a problem hiding this comment.
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (2)
src/specify_cli/init.py:763
- In
--forcemode,shutil.copy2(src_path, dst_path)will follow an existing destination symlink and overwrite the symlink target (potentially outside the project). This enables a symlink-attack scenario if a repo contains.specify/...symlinks and the user runsinit --force/integration upgrade --force. Guard againstdst_path.is_symlink()(unlink it or error) so overwrites never follow symlinks.
This issue also appears on line 778 of the same file.
else:
dst_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_path, dst_path)
rel = dst_path.relative_to(project_path).as_posix()
manifest.record_existing(rel)
src/specify_cli/init.py:782
- When
force=Trueanddstis a symlink,shutil.copy2will follow it and overwrite the target path. Please add a guard fordst.is_symlink()(unlink it or error) so--forcecannot write outside the project via symlinked template paths.
if dst.exists() and not force:
skipped_files.append(str(dst.relative_to(project_path)))
else:
shutil.copy2(f, dst)
rel = dst.relative_to(project_path).as_posix()
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
_install_shared_infra()previously skipped all existing files under.specify/scripts/and.specify/templates/, regardless of--force. This meant users could never receive upstream fixes to shared scripts or templates after initial project setup.Changes
forceparameter to_install_shared_infra()— whenTrue, existing files are overwritten with the latest bundled versions; whenFalse(default), only missing files are added (preserving backward compatibility)force=Truethrough CLI call sites —specify init --here --forceandspecify integration upgrade --forcenow passforce=True;integration installandintegration switchkeep the safe defaultlogging.warningwithconsole.print()output that lists skipped files and suggests--force--forceupdated shared infra (it didn't) and warned about template overwrites (they didn't happen); both are now accurateTests
6 new tests covering all scenarios:
specify init --here --forceoverwrites shared infraspecify init --here(no force) preserves shared infra and shows warningAll 1576 tests pass.
Fixes #2319