feat: add multi-language hook support (Phase 1 — Python)#7451
Conversation
b197eb0 to
09439e6
Compare
There was a problem hiding this comment.
Pull request overview
Adds a unified hook execution framework to enable non-shell “language hooks”, delivering Phase 1 Python support (auto-detection, venv + dependency install, and routing via a new executor interface) while keeping existing shell hook behavior intact.
Changes:
- Introduces
tools.HookExecutor+tools.ExecutionContextand updates bash/pwsh execution to the unified lifecycle (Prepare + Execute). - Adds
pkg/tools/language/with language inference, project-file discovery (walk-up), and a Python executor that manages venv/deps. - Extends hook config/schema/docs to support
languageanddir, plus adds Python-oriented error suggestion rules and broad new test coverage.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| schemas/v1.0/azure.yaml.json | Adds language/dir to hook schema and adjusts validation rules. |
| schemas/alpha/azure.yaml.json | Same schema updates for alpha. |
| cli/azd/resources/error_suggestions.yaml | Adds Python hook-related suggestion rules. |
| cli/azd/pkg/tools/script.go | Replaces prior exec options with ExecutionContext and defines HookExecutor. |
| cli/azd/pkg/tools/powershell/powershell.go | Refactors PowerShell execution into a HookExecutor with Prepare-time runtime resolution. |
| cli/azd/pkg/tools/powershell/powershell_test.go | Updates tests for new PowerShell executor lifecycle. |
| cli/azd/pkg/tools/language/executor.go | Adds ScriptLanguage constants and extension-based language inference. |
| cli/azd/pkg/tools/language/executor_test.go | Tests language inference behavior. |
| cli/azd/pkg/tools/language/project_discovery.go | Adds upward project discovery bounded by a root directory. |
| cli/azd/pkg/tools/language/project_discovery_test.go | Tests project discovery behavior and boundary enforcement. |
| cli/azd/pkg/tools/language/python_executor.go | Implements Python hook executor (runtime check, venv, pip install, execution). |
| cli/azd/pkg/tools/language/python_executor_test.go | Unit tests for Python executor behavior. |
| cli/azd/pkg/tools/bash/bash.go | Refactors bash execution into a HookExecutor (Prepare no-op). |
| cli/azd/pkg/tools/bash/bash_test.go | Updates tests for new bash executor lifecycle. |
| cli/azd/pkg/ext/models.go | Adds Language/Dir to hook config and updates validation/language resolution. |
| cli/azd/pkg/ext/models_test.go | Adds coverage for language resolution, dir inference, and inline rejection. |
| cli/azd/pkg/ext/hooks_runner.go | Routes hooks through IoC-resolved executors and constructs per-hook ExecutionContext. |
| cli/azd/pkg/ext/hooks_runner_test.go | Updates runner tests and adds language hook execution path coverage. |
| cli/azd/pkg/ext/hooks_manager.go | Adds early runtime validation for language hooks (Phase 1: Python). |
| cli/azd/pkg/ext/hooks_manager_test.go | Tests runtime validation behavior for Python installed/missing and mixed hooks. |
| cli/azd/pkg/ext/python_hooks_e2e_test.go | Adds end-to-end tests covering Python hook pipeline behaviors. |
| cli/azd/docs/language-hooks.md | Documents language hooks, config, and Python Phase 1 behavior. |
| cli/azd/cmd/container.go | Registers hook executors as named transients for IoC resolution. |
| cli/azd/cmd/hooks.go | Updates azd hooks run to pass ExecutionContext options. |
| cli/azd/cmd/hooks_test.go | Updates tests and adds executor registration helper. |
| cli/azd/cmd/middleware/hooks_test.go | Updates middleware tests to register executors in the mock container. |
| cli/azd/cover | Adds a Go coverage artifact file. |
jongio
left a comment
There was a problem hiding this comment.
CI is failing across golangci-lint, go-fix, and build/test (Linux/Mac/Windows) - that needs to be green before merge.
Issues to address:
- pkg/ext/hooks_runner.go:172 - file-based shell hooks silently get cwd changed from project root to script dir (breaking)
- pkg/tools/language/python_executor.go:104 - Python executor creates a venv when the nearest project file is JS/.NET, not Python
- pkg/tools/powershell/powershell.go:91 - PS5 fallback lost the execution-failure diagnostic
- pkg/ext/hooks_runner.go:222 - ErrorWithSuggestion missing Links field
Architecture and lifecycle design (Prepare/Execute/Cleanup with IoC) looks solid. Test coverage is thorough.
…ring, temp file dedup) - Rename schema property 'language' to 'kind' to match HookConfig struct - Add containment check on resolvedScriptPath to prevent path traversal - Move defer Cleanup() before Prepare() for defensive cleanup - Extract CreateInlineTempScript() shared helper from bash/powershell Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cd54d37 to
097ed07
Compare
jongio
left a comment
There was a problem hiding this comment.
Solid architecture - the HookExecutor lifecycle and IoC-based resolution fit the codebase well. All previous threads are resolved. Three minor improvement suggestions below.
- Wrap defer Cleanup() in closure to log errors instead of silently dropping - Add copylocks to cspell overrides for project.go Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jongio
left a comment
There was a problem hiding this comment.
Clean architecture. IoC-based executor resolution is extensible - future languages just register a named transient. Path containment guards, error_suggestions entries, and the backward-compat path from shell: to kind: are all solid. Test coverage is thorough.
Azure Dev CLI Install InstructionsInstall scriptsMacOS/Linux
bash: pwsh: WindowsPowerShell install MSI install Standalone Binary
MSI
Documentationlearn.microsoft.com documentationtitle: Azure Developer CLI reference
|
The path containment check introduced in multi-language hooks (PR #7451) incorrectly used the service directory as the boundary, rejecting legitimate relative paths like ../../hooks/script.ps1 from service-level hooks. This changes the boundary to the project root directory where azure.yaml lives. Fixes #7666 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: use project root as boundary for hook path containment (#7666) The path containment check introduced in multi-language hooks (PR #7451) incorrectly used the service directory as the boundary, rejecting legitimate relative paths like ../../hooks/script.ps1 from service-level hooks. This changes the boundary to the project root directory where azure.yaml lives. Fixes #7666 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: correct hook kind inference when dir and run are both set When both dir and run fields are configured on a hook, the kind inference used the raw run value instead of the actual file found on disk. This caused hooks without explicit kind to default to the OS shell (PowerShell on Windows) instead of inferring the correct kind from the file extension. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: exempt inline hooks from path containment checks Inline hooks are saved to OS temp files for execution, so their script path and working directory are expected to be outside the project root. Only file-based hooks enforce the project root containment boundary. Also fixes doc comment on NewHooksManager and handles os.Getwd error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: split validate() into phases and rename private fields Split the monolithic validate() method into four focused phases: parseRunTarget, resolveKind, resolvePaths, and enforceContainment. Rename private fields for clarity: path -> relativeScriptPath, cwd -> inputCwd, script -> inlineScript. No behavior change -- purely internal readability improvement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: use HooksManagerOptions struct for NewHooksManager Replace two adjacent string parameters (cwd, projectDir) with a HooksManagerOptions struct to prevent accidental argument swapping and make call sites self-documenting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: account for Dir field in ValidateHooks file detection ValidateHooks() ignored the Dir field when checking if a hook script exists on disk, causing dir+run combinations to be incorrectly classified as inline scripts and defaulting to the OS shell (PowerShell on Windows). The file detection now checks dir+run when Dir is set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: add executor dispatch tests for kind/run/dir combinations Table-driven tests verify that every valid combination of kind, run, and dir fields resolves to the correct executor Kind after validation. Covers all 6 hook kinds for run-only and dir+run patterns, inline script defaults, and explicit kind overrides. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove duplicate file detection from ValidateHooks ValidateHooks() had its own file detection logic that ignored the Dir field, causing dir+run combinations to be misclassified as inline scripts. This pre-set Kind to the OS default shell before validate() ran, preventing correct kind inference from file extensions. validate() is now the sole authority for Kind assignment. ValidateHooks calls validate() first and reads post-validation state for warnings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: ensure shared validation pipeline is kind-agnostic The shared hook validation pipeline (resolveScript, resolveKind, resolvePaths, enforceContainment) treats all 6 hook kinds uniformly. Kind-specific validation (runtime checks, dependency discovery) lives in each executor's Prepare() method. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Adds multi-language hook support to azd, starting with Python as the first non-shell language. This extends the existing hook system (previously bash/PowerShell only) with a unified
HookExecutorarchitecture that supports auto-detection, virtual environment management, and automatic dependency installation.Related Issues
User Guide: Python Hooks
Quick Start
Add a Python script as a hook — azd automatically detects the
.pyextension, creates a virtual environment, installs dependencies, and runs the script:Place a
requirements.txtalongside your script:That's it —
azd provisionwill automatically:.pyextensionhooks_env/)requirements.txtsetup.pywith azd environment variables availableConfiguration Options
run.pyscript file (relative to project root)kindpython,sh,pwshdirExamples
Auto-detected (simplest):
Script in subdirectory:
Override dir for shared dependencies:
Platform-specific hooks:
Virtual Environment Handling
azd automatically manages Python virtual environments:
VIRTUAL_ENVenvironment variable.venv/directory (withpyvenv.cfg)venv/directory (withpyvenv.cfg){dirName}_env/alongside the scriptDependency Installation
azd walks up the directory tree from the script location to find dependency files:
pyproject.tomlpip install .(PEP 621 project install)requirements.txtpip install -r requirements.txtpyproject.tomltakes priority overrequirements.txtif both exist.Requirements
venvmodulesudo apt install python3-venvPATH(python3on Unix,py/pythonon Windows)Limitations
run: ./script.py), not inline codeArchitecture Changes
HookExecutorinterface (pkg/tools/script.go): Unified 3-phase lifecycle —Prepare()/Execute()/Cleanup()serviceLocator.ResolveNamed()HookKindtype (pkg/tools/language/executor.go): Type-safe executor selection (sh,pwsh,python)validate()with containment checks against project boundaryCreateInlineTempScript(): Centralized temp file creation for bash/powershell inline scriptsFiles Added
pkg/tools/language/executor.go— HookKind type and constantspkg/tools/language/python_executor.go— Python executor with venv/pip lifecyclepkg/tools/language/project_discovery.go— Walk-up project file finderpkg/ext/python_hooks_e2e_test.go— E2E testsdocs/language-hooks.md— Feature documentationFiles Modified
pkg/tools/script.go— HookExecutor interface, ExecutionContext, CreateInlineTempScriptpkg/ext/models.go— HookConfig with Kind/Dir fields, path resolution, containment checkspkg/ext/hooks_runner.go— Unified execHook() with IoC resolutionpkg/tools/bash/bash.go/powershell.go— Executor implementations using shared helpercmd/container.go— IoC registration for hook executorsschemas/v1.0/azure.yaml.json/schemas/alpha/azure.yaml.json— Schema updates