Skip to content

feat(integrations): add a preflight_setup() hook that runs before shared writes - #5198

Open
mvanhorn wants to merge 2 commits into
github:mainfrom
mvanhorn:feat/integration-preflight-setup
Open

mvanhorn wants to merge 2 commits into
github:mainfrom
mvanhorn:feat/integration-preflight-setup

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

Description

Prerequisite for #5032, split out as the integration delivery boundary in CONTRIBUTING.md and design/integration.md asks.

integration install, switch and upgrade write shared templates and scripts before they call the integration's setup(). If setup() 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 raises ValueError or OSError, 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() into shared_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.md documents the hook under Ownership and lifecycle.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (if applicable)

New tests/specify_cli/integrations/test_preflight_setup.py runs the real CLI against two existing built-in integrations and uses no OpenCode code:

  • A refusal (ValueError and PermissionError) 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.
  • The accepting path calls the hook once with the integration, project root, parsed options and script_type="sh", and the command succeeds.
  • The default hook returns 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 tests passes, markdownlint-cli2 design/integration.md is clean, and uv run pytest on the new file plus test_command_install.py, test_command_switch.py, test_command_upgrade.py and the shared-infra tests gives 147 passed.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (fill in the disclosure below)

AI disclosure: AI assistance was used to split this change out of #5032, write the tests and draft this description, under human review.

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.
@mvanhorn
mvanhorn requested a review from mnriem as a code owner October 30, 2026 07:27
@mnriem mnriem added the triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review label Oct 30, 2026
@mnriem
mnriem requested an auto review from Copilot October 30, 2026 13:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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)

@mnriem

mnriem commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

@mnriem mnriem added the author-awaiting Waiting on author response label Oct 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author-awaiting Waiting on author response triage-nice-to-have Verdict: evidence-backed fix or greenlit feature — land after review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants