mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix(extensions): make shipped scripts executable after install (#3723)
Extension archives are unpacked with zipfile.extractall and directory installs are copied; neither restores a stripped Unix mode. A bundled *.sh therefore lands non-executable, so a documented `.specify/extensions/<id>/scripts/bash/foo.sh` invocation fails with "Permission denied" — e.g. a CI step that runs an extension's gate. It only worked incidentally, after a later `specify init`. Restore permissions at the shared sink. Every extension install route funnels through ExtensionManager.install_from_directory (install_from_zip delegates to it; extension add, extension update, and bundle installs all reach it), so calling the existing ensure_executable_scripts() there covers every route — present and future — by construction rather than by patching each command. The helper already makes .specify scripts executable (init, migrate, and integration-install all call it); it is called plainly, re-establishing the same idempotent "scripts are executable" invariant those flows restore. Deliberately the whole-project call rather than a scoped one: a scan-scope argument would only spare re-walking already-correct files — negligible beside the copy/extract just performed — while widening a simple, widely-used interface for a single caller. Existing callers were audited: init's end-of-init call still covers core .specify/scripts and is untouched; integration-install and migrate do no manager install. Nothing is removed. No-op on Windows; best-effort per file; does not change which files are executable or their mode. Tests: a manager-level regression test asserts a mode-0644 script comes out executable via both install_from_directory and install_from_zip(force=True) (the latter also covering the remove-then-reinstall shape of extension update), plus an end-to-end `extension add --dev` test. Both fail without the change; skipped on Windows. Fixes #3722.
This commit is contained in:
@@ -2021,6 +2021,24 @@ class ExtensionManager:
|
||||
except OSError:
|
||||
pass # Best-effort; install already committed to the registry.
|
||||
|
||||
# Restore execute bits on shipped POSIX scripts. copytree here (and the
|
||||
# zipfile.extractall in install_from_zip, which delegates to this method) does
|
||||
# not restore a stripped Unix mode, so a bundled *.sh would land non-executable
|
||||
# and a documented `.specify/extensions/<id>/scripts/...` invocation would fail
|
||||
# with "Permission denied". This is the single sink every install route funnels
|
||||
# through (extension add / update / bundle), so fixing it here covers them all.
|
||||
# No-op on Windows (helper returns early).
|
||||
#
|
||||
# Deliberately the whole-project call, not a scoped one. The helper's contract
|
||||
# is "make .specify scripts executable" — the same idempotent invariant that
|
||||
# init and migrate restore — so re-establishing it after an install is exactly
|
||||
# its job. A scoped variant would only spare re-walking already-correct files,
|
||||
# a handful of stats that are negligible beside the copy/extract this method just
|
||||
# did, and it would cost a per-caller scan-scope argument on an otherwise simple,
|
||||
# widely-used interface. The simpler call wins.
|
||||
from .. import ensure_executable_scripts
|
||||
ensure_executable_scripts(self.project_root)
|
||||
|
||||
return manifest
|
||||
|
||||
def install_from_zip(
|
||||
|
||||
Reference in New Issue
Block a user