Repository navigation
Conversation
Install, switch and upgrade now call IntegrationBase.preflight_setup() after options and the script type are resolved and before any shared template, script or integration file is written. A ValueError or OSError from the hook ends the command with exit 1 and "No files were changed." The default accepts every setup, so no integration changes. Also expose shared_infra.script_variant_dirs() so a hook can see which script variants install_shared_infra() will copy. Split out of github#5032 per the integration delivery boundary.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The preflight tests do not verify that non-empty integration options reach the hook, so dropping parsed options from the hook invocation would go undetected.
1 open issue
What changed in this PR
Adds an integration preflight hook so install, switch, and upgrade can reject unsupported project conditions before shared files change. This provides the lifecycle boundary needed by the proposed OpenCode configuration work.
Changes:
- Adds a default no-op
preflight_setup()hook and consistent refusal handling across three commands. - Extracts script-variant selection into a reusable helper without changing behavior.
- Documents the hook and adds acceptance, refusal, and script-selection tests.
| File | Description |
|---|---|
tests/specify_cli/integrations/test_preflight_setup.py |
Tests hook invocation, refusal without writes, and script variants. |
src/specify_cli/shared_infra.py |
Extracts reusable script-variant selection. |
src/specify_cli/integrations/command_upgrade.py |
Runs preflight before upgrade writes. |
src/specify_cli/integrations/command_switch.py |
Runs target preflight before removing the current integration. |
src/specify_cli/integrations/command_install.py |
Runs preflight before installing shared infrastructure. |
src/specify_cli/integrations/base.py |
Defines the default preflight hook. |
src/specify_cli/integrations/_helpers.py |
Centralizes hook invocation and refusal reporting. |
design/integration.md |
Documents the preflight lifecycle contract. |
Review effort: Balanced (auto)
Collaborator
|
Please address Copilot feedback |
This branch has not been deployed
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.

Description
Prerequisite for #5032, split out as the integration delivery boundary in
CONTRIBUTING.mdanddesign/integration.mdasks.integration install,switchandupgradewrite shared templates and scripts before they call the integration'ssetup(). Ifsetup()then rejects the project (for example because a file it has to edit is a symlink or unwritable), the shared files have already changed, and a built-in-only upgrade has no journal to roll them back. An integration has no way to say no before that point.This adds
IntegrationBase.preflight_setup(project_root, parsed_options, *, script_type). The three commands call it once options and the script type are resolved and before any write. If it raisesValueErrororOSError, the command prints the error and "No files were changed." and exits 1. The default implementation accepts everything, so no existing integration behaves differently.It also moves the script-variant choice in
install_shared_infra()intoshared_infra.script_variant_dirs(), so a hook can check exactly the script variants that will be installed. That part is a refactor with no behavior change.design/integration.mddocuments the hook under Ownership and lifecycle.Testing
uv run specify --helpuv sync && uv run pytestNew
tests/specify_cli/integrations/test_preflight_setup.pyruns the real CLI against two existing built-in integrations and uses no OpenCode code:ValueErrorandPermissionError) from install, switch and upgrade exits 1 with "No files were changed.", and every file under the project is byte-identical afterwards, even with a stale shared template that a run past the hook would rewrite.script_type="sh", and the command succeeds.None;script_variant_dirs()returns the same variants as before.The refusal and call tests fail on main with the CLI wiring removed (9 failures) and pass with it. Locally on macOS:
uvx ruff@0.15.0 check src testspasses,markdownlint-cli2 design/integration.mdis clean, anduv run pyteston the new file plustest_command_install.py,test_command_switch.py,test_command_upgrade.pyand the shared-infra tests gives 147 passed.AI Disclosure
AI disclosure: AI assistance was used to split this change out of #5032, write the tests and draft this description, under human review.