Documentation
¶
Overview ¶
Package hooks provides hook execution and management for Hitch operations.
Overview ¶
This package implements a flexible hook system that allows users to execute custom commands at specific points in the Hitch workflow. Hooks enable integration with CI/CD systems, testing frameworks, notification services, and other automation tools.
Hook Types ¶
Four hook types are supported, corresponding to major Hitch operations:
- pre-promote: Runs before a feature is promoted to an environment
- post-promote: Runs after a feature is successfully promoted
- pre-release: Runs before releasing an environment to base branch
- post-release: Runs after a successful release
Hook Configuration ¶
Hooks are defined in hitch.json:
{
"hooks": {
"pre-promote": [
{
"name": "run-tests",
"type": "command",
"command": "./scripts/test.sh",
"args": ["{{.Branch}}"],
"required": true
}
],
"post-promote": [
{
"name": "notify-slack",
"type": "command",
"command": "curl",
"args": ["-X", "POST", "https://hooks.slack.com/...",
"-d", "{\"text\": \"{{.Branch}} promoted to {{.Environment}}\"}"],
"required": false
}
]
}
}
Hook Execution ¶
Execute hooks programmatically:
import (
"context"
"github.com/DoomedRamen/hitch/internal/hooks"
)
executor := hooks.NewHookExecutor(hooksConfig)
// Create hook context
ctx := hooks.HookContext{
Branch: "feature/login",
Environment: "dev",
UserEmail: "user@example.com",
UserName: "John Doe",
BaseBranch: "main",
}
// Execute pre-promote hooks
results := executor.ExecuteHooks(context.Background(), hooks.PrePromote, ctx)
// Check results
for _, result := range results {
if !result.Success && result.Hook.Required {
return fmt.Errorf("required hook '%s' failed: %v", result.Hook.Name, result.Error)
}
}
Hook Configuration Fields ¶
Each hook supports these fields:
- name: Descriptive name for the hook (e.g., "run-tests")
- type: Must be "command" (builtin hooks are deprecated)
- command: The executable to run (e.g., "./scripts/test.sh", "curl", "npm")
- args: Array of arguments passed to the command
- working_dir: Optional working directory for execution
- required: If true, hook failure blocks the operation
Template Variables ¶
Hook commands and arguments support template substitution:
- {{.Branch}}: The feature branch name (e.g., "feature/login")
- {{.Environment}}: The environment name (e.g., "dev", "qa")
- {{.UserEmail}}: Git user email (e.g., "user@example.com")
- {{.UserName}}: Git user name (e.g., "John Doe")
- {{.BaseBranch}}: The base branch (e.g., "main")
Example usage in hook configuration:
{
"name": "deploy",
"command": "./deploy.sh",
"args": ["{{.Environment}}", "{{.Branch}}"],
"working_dir": "/opt/deployment"
}
Required vs Optional Hooks ¶
Hook behavior depends on the required field:
Required hooks (required: true):
- Must succeed for the operation to continue
- Failure blocks the promote/release operation
- Used for critical validations (tests, security scans)
Optional hooks (required: false):
- Failure is logged but doesn't block operation
- Used for notifications, metrics, non-critical tasks
Hook Results ¶
Each hook execution returns a HookResult:
type HookResult struct {
Hook HookConfig // The hook configuration
Success bool // Whether execution succeeded
Output string // Combined stdout/stderr
Error error // Error if execution failed
}
Execution Flow ¶
Hook execution follows this pattern:
- Load hook configuration from hitch.json
- Create HookExecutor with configuration
- Build HookContext with operation details
- Execute hooks of specific type (pre-promote, post-promote, etc.)
- For each hook: - Replace template variables in command/args - Execute command with timeout - Capture output and exit status - If required hook fails, stop execution
- Return array of HookResults
Example Hooks ¶
Run tests before promoting:
{
"name": "unit-tests",
"type": "command",
"command": "npm",
"args": ["test"],
"required": true
}
Notify Slack after promotion:
{
"name": "slack-notify",
"type": "command",
"command": "./scripts/notify.sh",
"args": ["{{.Branch}}", "{{.Environment}}"],
"required": false
}
Deploy to environment after promotion:
{
"name": "auto-deploy",
"type": "command",
"command": "kubectl",
"args": ["apply", "-f", "k8s/{{.Environment}}.yaml"],
"working_dir": "/path/to/repo",
"required": true
}
Context and Cancellation ¶
Hooks respect context cancellation:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() results := executor.ExecuteHooks(ctx, hooks.PrePromote, hookCtx)
If context is cancelled, running hooks are terminated.
Error Handling ¶
Hook failures are handled gracefully:
- Non-zero exit code: Marked as failure
- Command not found: Error returned with helpful message
- Timeout: Context deadline exceeded
- Required hook failure: Stops execution of remaining hooks
Deprecated Features ¶
Builtin hooks (type: "builtin") are no longer supported:
- Previously supported hooks like "test", "lint", "validate"
- Now users must define hooks explicitly using type: "command"
- Provides more flexibility and transparency
Testing ¶
Hook execution can be tested by examining results:
results := executor.ExecuteHooks(ctx, hooks.PrePromote, hookCtx)
for _, result := range results {
if !result.Success {
t.Errorf("Hook %s failed: %v", result.Hook.Name, result.Error)
}
}
Integration with Safety System ¶
Hooks integrate with Hitch's safety mechanisms:
- Hooks run during temp branch testing (safety.TempBranchTester)
- If hooks fail during testing, actual operation is prevented
- No changes are made to repository if required hooks fail
Best Practices ¶
- Use required: true for validation hooks (tests, linting, security scans)
- Use required: false for notifications and metrics
- Keep hooks fast - long-running hooks slow down operations
- Test hooks in isolation before adding to hitch.json
- Use template variables to avoid hardcoding values
- Provide clear error messages in hook scripts
- Document hooks in README for team members
Example: Complete Hook Setup ¶
{
"hooks": {
"pre-promote": [
{
"name": "validate-branch",
"type": "command",
"command": "./scripts/validate.sh",
"args": ["{{.Branch}}"],
"required": true
},
{
"name": "run-tests",
"type": "command",
"command": "npm",
"args": ["test"],
"required": true
}
],
"post-promote": [
{
"name": "deploy-dev",
"type": "command",
"command": "./deploy.sh",
"args": ["{{.Environment}}"],
"required": false
},
{
"name": "notify-team",
"type": "command",
"command": "curl",
"args": ["-X", "POST", "https://notify.example.com",
"-d", "{\"branch\": \"{{.Branch}}\", \"env\": \"{{.Environment}}\"}"],
"required": false
}
]
}
}
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type HookConfig ¶
type HookConfig struct {
Name string `yaml:"name"`
Type string `yaml:"type"` // "builtin" or "command"
Command string `yaml:"command"` // for type=command
WorkingDir string `yaml:"working_dir"` // optional working directory
Args []string `yaml:"args"` // arguments for the command
Config map[string]string `yaml:"config"` // configuration for builtin hooks
Required bool `yaml:"required"` // whether failure should block operation
}
HookConfig represents a single hook configuration
type HookContext ¶
type HookContext struct {
Branch string
Environment string
UserEmail string
UserName string
BaseBranch string
}
HookContext provides context to hook execution
type HookExecutor ¶
type HookExecutor struct {
// contains filtered or unexported fields
}
HookExecutor handles hook execution
func NewHookExecutor ¶
func NewHookExecutor(config *HooksConfig) *HookExecutor
NewHookExecutor creates a new hook executor
func (*HookExecutor) ExecuteHooks ¶
func (he *HookExecutor) ExecuteHooks(ctx context.Context, hookType HookType, hookCtx HookContext) []HookResult
ExecuteHooks executes hooks of a specific type
type HookResult ¶
type HookResult struct {
Hook HookConfig
Success bool
Output string
Error error
}
HookResult represents the result of hook execution
type HookType ¶
type HookType string
HookType represents different types of hooks
const ( PreDemote HookType = "pre-demote" PostDemote HookType = "post-demote" PrePromote HookType = "pre-promote" PostPromote HookType = "post-promote" PreRelease HookType = "pre-release" PostRelease HookType = "post-release" PreMetadataWrite HookType = "pre-metadata-write" PostMetadataWrite HookType = "post-metadata-write" PreRebuild HookType = "pre-rebuild" PostRebuild HookType = "post-rebuild" ConflictDetected HookType = "conflict-detected" ResumeRebuild HookType = "resume-rebuild" )
type HooksConfig ¶
type HooksConfig struct {
PreDemote []HookConfig `yaml:"pre-demote,omitempty"`
PostDemote []HookConfig `yaml:"post-demote,omitempty"`
PrePromote []HookConfig `yaml:"pre-promote,omitempty"`
PostPromote []HookConfig `yaml:"post-promote,omitempty"`
PreRelease []HookConfig `yaml:"pre-release,omitempty"`
PostRelease []HookConfig `yaml:"post-release,omitempty"`
PreMetadataWrite []HookConfig `yaml:"pre-metadata-write,omitempty"`
PostMetadataWrite []HookConfig `yaml:"post-metadata-write,omitempty"`
PreRebuild []HookConfig `yaml:"pre-rebuild,omitempty"`
PostRebuild []HookConfig `yaml:"post-rebuild,omitempty"`
ConflictDetected []HookConfig `yaml:"conflict-detected,omitempty"`
ResumeRebuild []HookConfig `yaml:"resume-rebuild,omitempty"`
}
HookConfig represents the complete hook configuration
func LoadConfig ¶
func LoadConfig(meta *metadata.Metadata) (*HooksConfig, error)
LoadConfig loads hook configuration from metadata