feat: add multi-language hook support (Phase 1 — Python)#7451
feat: add multi-language hook support (Phase 1 — Python)#7451wbreza wants to merge 9 commits intoAzure:mainfrom
Conversation
Extend the azd hook system to support programming language hooks beyond bash and PowerShell. Phase 1 delivers the extensible framework and Python support. New capabilities: - ScriptExecutor interface for language-based hook execution - Auto-detect language from file extension (.py, .js, .ts, .cs) - Optional explicit language: and dir: fields in azure.yaml - Python executor with virtual environment and pip dependency management - Project file discovery (walks up from script to find requirements.txt) - Early runtime validation with actionable install guidance - Full backwards compatibility with existing shell hooks New files: - pkg/tools/language/executor.go — ScriptExecutor interface, ScriptLanguage type - pkg/tools/language/python_executor.go — Python lifecycle (venv, pip, execute) - pkg/tools/language/project_discovery.go — Walk-up project file finder - docs/language-hooks.md — Feature documentation Modified files: - pkg/ext/models.go — HookConfig Language/Dir fields, language resolution - pkg/ext/hooks_runner.go — Language hook execution pipeline - pkg/ext/hooks_manager.go — Runtime validation - schemas/v1.0/azure.yaml.json — Schema updates - schemas/alpha/azure.yaml.json — Schema updates - resources/error_suggestions.yaml — Python hook error rules Contributes to Azure#4384. Complements Azure#7423. Extends patterns from Azure#3613, Azure#4740, Azure#4560. Motivated by research in Azure#3978. Extends detection from Azure#1579. Part of Azure#7435. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix British spelling "behaviour" → "behavior" in hooks_runner.go and models.go - Tighten WriteFile permissions from 0o644 to 0o600 in models_test.go (gosec G306) - Break long test line to stay within 125-char limit (lll) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Swap pyproject.toml before requirements.txt in project discovery, matching the convention in framework_service_python.go and internal/appdetect/python.go (PEP 621 preference) - Add PATH validation to resolvePythonCmd fallback - Add explanatory comments on project discovery design Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the dir field is not explicitly set on a language hook, it is
now automatically inferred from the directory containing the script
referenced by run. This eliminates redundant configuration:
Before (both required):
run: hooks/preprovision/main.py
dir: hooks/preprovision
After (dir auto-inferred):
run: hooks/preprovision/main.py
The dir field remains available as an optional override when the
project root differs from the script's directory.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add ErrUnsupportedLanguage and ErrShellLanguage to excludedErrors in errors_test.go (internal hook routing errors caught in hooks_runner.go) - Fix Test_ServiceHooks_Registered mock expectations for refactored hook execution pipeline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the dual execution pipeline (execShellHook / execLanguageHook) with a single unified flow through tools.ScriptExecutor. All hook types - bash, PowerShell, and Python - now implement the same two-phase interface: Prepare() + Execute(). This eliminates branching in the hooks runner and makes adding new languages a single-file change. Key changes: - Move ScriptExecutor interface to pkg/tools/script.go (was in language/) - Bash: add no-op Prepare(), implement ScriptExecutor - PowerShell: move pwsh/powershell fallback from Execute to Prepare - Remove GetScript(), execShellHook(), execLanguageHook() - Single execHook() path for all hook types - Remove ErrShellLanguage (shells are now just another executor) - Remove UserPwsh from ExecOptions (resolved in PS constructor) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace manual executor factory with IoC named resolution. The hooks runner no longer constructs language-specific dependencies — executors are registered in container.go and resolved at runtime. Renames: - ScriptExecutor → HookExecutor - ExecOptions → folded into ExecutionContext - ScriptContext concept → ExecutionContext Key changes: - Executor constructors take only IoC-injectable deps (CommandRunner, python.Cli) - Per-invocation data (Cwd, EnvVars, BoundaryDir, Interactive, StdOut) flows via ExecutionContext - Named transient registration in container.go by language - Hooks runner resolves via serviceLocator.ResolveNamed() - Remove GetExecutor() factory and language-specific imports from hooks_runner Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Test_ServiceHooks_ValidationUsesServicePath test from main was missing the registerHookExecutors call needed for IoC resolution of HookExecutor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
b197eb0 to
09439e6
Compare
- Register hook executors (bash, powershell, python) in IoC container for tests that call hooksRunAction.Run(), fixing 'no concrete found for: tools.HookExecutor' errors in 3 layer hook tests - Update ValidatesLayerHooksRelativeToLayerPath assertion: validate() now infers ShellTypeBash from .sh file extension via validateLanguageRuntimes, so Shell is 'sh' not ScriptTypeUnknown Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Azure Dev CLI Install InstructionsInstall scriptsMacOS/Linux
bash: pwsh: WindowsPowerShell install MSI install Standalone Binary
MSI
Documentationlearn.microsoft.com documentationtitle: Azure Developer CLI reference
|
Summary
Extends the azd hook system to support programming language hooks beyond bash and PowerShell. Phase 1 delivers the extensible
ScriptExecutorframework and full Python support — including auto-detection from file extensions, virtual environment management, automatic dependency installation via pip, and early runtime validation with actionable install guidance.Template consumers can now use Python hooks that "just work" during
azd upwithout manual setup.Changes
New package:
pkg/tools/language/executor.go—ScriptExecutorinterface,ScriptLanguagetype with constants,InferLanguageFromPath()helper,GetExecutor()factory with sentinel errors for unsupported languagespython_executor.go— Full Python lifecycle: runtime check → project discovery → venv creation → pip install → script execution. UsespythonToolsinterface for testabilityproject_discovery.go—DiscoverProjectFile()walks up from script directory to findrequirements.txt,pyproject.toml,package.json,*.*proj, bounded by project rootHook configuration:
pkg/ext/models.go— AddedLanguageandDirfields toHookConfig. Language resolution invalidate()with priority: explicitlanguage:→shell:mapping → file extension → OS default.IsLanguageHook()helper. Inline scripts rejected for non-shell languageshooks_runner.go— NewexecLanguageHook()pipeline alongside existingexecShellHook(). Shared helpers for exec options and error handling. Shell hooks completely unchangedhooks_manager.go—validateLanguageRuntimes()for early runtime-missing detection withErrorWithSuggestionpattern (extends core: Hardening/Fail-fast for projects with hooks using pwsh #4740/[Issue] warn users when they need to install powershell 7 (or fallback to powershell 5) #4560 approach)Schema & documentation
schemas/v1.0/azure.yaml.jsonandschemas/alpha/azure.yaml.json— Addedlanguage(enum) anddir(string) properties to hook config schemaresources/error_suggestions.yaml— 4 new Python hook error suggestion rulesdocs/language-hooks.md— Feature documentation with examplesTests (159 total, 0 failures)
executor_test.go— Language inference, executor factorypython_executor_test.go— Prepare/Execute with mocked dependencies (venv, pip, runtime check)project_discovery_test.go— Walk-up discovery, boundary enforcement, priority, edge casesmodels_test.go— Language resolution, YAML deserialization, inline rejectionhooks_runner_test.go— Language hook routing, env vars, error handlinghooks_manager_test.go— Runtime validation for installed/missing Pythonpython_hooks_e2e_test.go— 15 end-to-end tests covering full pipelineIssue References
Resolves #7435
Contributes to #4384
Relates to #7423
Relates to #3613
Relates to #4740
Relates to #4560
Relates to #3978
Relates to #1579
Testing
go build ./...— clean compilationgo test ./pkg/ext/... ./pkg/tools/language/... -count=1— 159 tests pass, 0 failuresgofmt -l .— no formatting issues-raceflagNotes
ErrUnsupportedLanguage— implementation tracked in the Multi-Language Hook Support: Python, JavaScript, TypeScript, .NET #7435 epic