Skip to content

fix: --force now overwrites shared infra files during init and upgrade#2320

Merged
mnriem merged 2 commits intogithub:mainfrom
mnriem:fix/shared-infra-force-overwrite
Apr 22, 2026
Merged

fix: --force now overwrites shared infra files during init and upgrade#2320
mnriem merged 2 commits intogithub:mainfrom
mnriem:fix/shared-infra-force-overwrite

Conversation

@mnriem
Copy link
Copy Markdown
Collaborator

@mnriem mnriem commented Apr 22, 2026

_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; when False (default), only missing files are added (preserving backward compatibility)
  • Wire force=True through CLI call sitesspecify init --here --force and specify integration upgrade --force now pass force=True; integration install and integration switch keep the safe default
  • Visible console warning — replace hidden logging.warning with console.print() output that lists skipped files and suggests --force
  • Fix contradictory upgrade docs — the docs claimed --force updated shared infra (it didn't) and warned about template overwrites (they didn't happen); both are now accurate

Tests

6 new tests covering all scenarios:

  • Unit: skip behavior without force, overwrite behavior with force
  • Unit: warning displayed when files skipped, no warning when forced
  • E2E: specify init --here --force overwrites shared infra
  • E2E: specify init --here (no force) preserves shared infra and shows warning

All 1576 tests pass.

Fixes #2319

_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
Copilot AI review requested due to automatic review settings April 22, 2026 21:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = False parameter to _install_shared_infra() to allow overwriting existing shared infra files when explicitly requested.
  • Wire force through specify init and specify integration upgrade so --force actually 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

Comment thread src/specify_cli/__init__.py Outdated
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'.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --force mode, 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 runs init --force/integration upgrade --force. Guard against dst_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=True and dst is a symlink, shutil.copy2 will follow it and overwrite the target path. Please add a guard for dst.is_symlink() (unlink it or error) so --force cannot 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

@mnriem mnriem merged commit 3970855 into github:main Apr 22, 2026
15 checks passed
@mnriem mnriem deleted the fix/shared-infra-force-overwrite branch April 22, 2026 21:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: specify init --here --force does not update existing .specify/scripts or .specify/templates files (upgrade instructions misleading)

2 participants