Repository navigation
Conversation
…tegration changes `integration use`, every `integration switch` path, and `integration uninstall` of the default integration refresh shared infrastructure through `_set_default_integration()`. The refresh rewrites `.specify/scripts/bash/*.sh` with the packaged mode (0644) and nothing restored the execute bits, so the scripts that generated commands and skills run directly failed with "Permission denied". Restore them after the refresh, as init, install and upgrade already do. Assisted-by: Codex (model: gpt-6.1-sol, autonomous) Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Human confirmation is needed because the shared transition changes permissions across use, switch, uninstall, and rollback, with interactions involving lifecycle journaling, partial-refresh recovery, and POSIX/Windows behavior.
No open issues 🎉
What changed in this PR
This PR fixes a permissions regression where integration changes rewrote shared POSIX scripts without execute permissions, causing directly invoked generated commands to fail. It restores executable bits after default-integration refreshes and adds regression coverage.
Changes:
- Restore execute permissions in
_set_default_integration()after shared infrastructure refreshes. - Test
integration switch,integration use, and default-integration fallback duringintegration uninstall. - Cover new, installed, and current-default switch paths, including refresh options.
| File | Description |
|---|---|
| tests/specify_cli/integrations/test_command_use.py | Verifies that integration use preserves executable permissions on shared scripts. |
| tests/specify_cli/integrations/test_command_uninstall.py | Verifies that fallback shared scripts remain executable after uninstalling the default integration. |
| tests/specify_cli/integrations/test_command_switch.py | Covers executable permissions across new, installed, and current-default integration switch paths. |
| src/specify_cli/integrations/_helpers.py | Restores executable permissions after default-integration shared infrastructure refreshes. |
Review effort: Lite (dynamic)
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
After
specify integration switch <key>,specify integration use <key>, orspecify integration uninstallof the default integration (when another installed one takes over), every script in.specify/scripts/bash/has mode 0644. The generated commands and skills tell the agent to run them directly (.specify/scripts/bash/setup-plan.sh --jsoninspeckit-plan, and so on), so on macOS and Linux each of those steps now fails withPermission denied(exit 126).All three commands go through
_set_default_integration()inintegrations/_helpers.py. It refreshes shared infrastructure with_install_shared_infra(..., refresh_managed=True), which writes the scripts with their packaged mode, 0644.init,integration install,integration upgradeand the first refresh inswitchcallensure_executable_scripts()after they write the scripts._set_default_integration()didn't. Inswitchthe existing call (command_switch.py:293) runs before the final default refresh, and that refresh writes the scripts a second time.This PR calls
ensure_executable_scripts()right after that refresh, the same way the other commands do and the way #3723 handledextension add. On Windows it does nothing, like the other call sites.Repro with the real CLI:
main(2ee2b1e)integration switch codexintegration install codex --script sh, thenintegration use codexintegration install codex --script sh, thenintegration uninstall claudeRelease 1.3.3 behaves the same for all three. #5377 added one more path:
switch <current default> --refresh-shared-infranow refreshes too, so it also drops the bits.With this change, the next
use,switchoruninstallalso fixes a project that is already affected. Onmain,specify integration upgradealready fixes them, since it callsensure_executable_scripts().On the switch path the console now prints "Updated execute permissions on 6 script(s) recursively" twice, once after each refresh. I kept the existing call in
command_switch.py: if the switch fails before the default is set, the scripts from the first refresh still get their bits.Testing
.venv/bin/specify --help.venv/bin/python -m pytest tests -qNew CLI tests, one per command, in the files
design/cli.mdassigns them to. Each checks the scripts are executable before the command and still executable after it:test_command_switch.py:TestIntegrationSwitch::test_switch_keeps_shared_scripts_executable, for a new target, an installed target, and the current default with--forceand with--refresh-shared-infratest_command_use.py:TestIntegrationUse::test_use_keeps_shared_scripts_executabletest_command_uninstall.py:TestIntegrationUninstall::test_uninstall_default_keeps_fallback_scripts_executableWith
_helpers.pyreverted tomain, all 6 cases fail, for exampleAssertionError: switch left shared scripts non-executable: ['check-prerequisites.sh', 'common.sh', 'create-new-feature.sh', 'resolve-template.sh', 'setup-plan.sh', 'setup-tasks.sh']. With the fix, all 6 pass. They are skipped on Windows, like #3723's test.Full suite on Linux, Python 3.13: 16273 passed, 566 skipped, 1 failed (
uv sync --extra test, thenpytest -n 2, 9 min). The failure,test_version_execution.py::TestWindowsUvToolEnvironment::test_non_windows_uv_tool_dry_run_still_previews_installer, fails the same way onmainhere: Rich wraps the dry-run output, and my checkout's venv path is long enough to be split mid-path. Ruff 0.15.0 (ruff check src tests) passes. I haven't run it on Windows or macOS.AI Disclosure
AI disclosure: Codex CLI (model: gpt-6.1-sol, reasoning effort xhigh, autonomous, offline sandbox) found the dropped execute bits while comparing release 1.3.3 and
mainon the same commands, and wrote a first patch. Claude Code (model: claude-opus-5-5, autonomous) traced every affected command to_set_default_integration(), rewrote the fix and the tests, reran the repro with the real CLI onmainand the branch, ran the full suite and lint, and drafted this description. The commit carriesAssisted-by:trailers for both.