Hook Examples¶
Copy-paste hook examples for common automation and integration scenarios.
đ Table of Contents¶
- đ Notifications
- đ Automation
- đ Reporting & Analytics
- đĄī¸ Governance & Validation
- đ External Integrations
- đī¸ Development Workflow
đ Notifications¶
Slack Notifications¶
Send rich Slack notifications for various ticket events.
Basic Slack Notification¶
#!/bin/bash
# .gira/hooks/ticket-created.sh
# Configure your Slack webhook URL
SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-https://hooks.slack.com/services/YOUR/WEBHOOK/URL}"
if [ -z "$SLACK_WEBHOOK_URL" ] || [ "$SLACK_WEBHOOK_URL" = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL" ]; then
echo "Slack webhook URL not configured"
exit 0
fi
# Send notification
curl -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{
\"text\": \"đĢ New ticket created: $GIRA_TICKET_ID\",
\"attachments\": [{
\"color\": \"good\",
\"title\": \"$GIRA_TICKET_TITLE\",
\"text\": \"$GIRA_TICKET_DESCRIPTION\",
\"fields\": [
{\"title\": \"Priority\", \"value\": \"$GIRA_TICKET_PRIORITY\", \"short\": true},
{\"title\": \"Type\", \"value\": \"$GIRA_TICKET_TYPE\", \"short\": true},
{\"title\": \"Assignee\", \"value\": \"$GIRA_TICKET_ASSIGNEE\", \"short\": true},
{\"title\": \"Labels\", \"value\": \"$GIRA_TICKET_LABELS\", \"short\": true}
]
}]
}"
Priority-Based Slack Notifications¶
#!/bin/bash
# .gira/hooks/ticket-created.sh
SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL}"
# Only notify for high priority tickets
if [ "$GIRA_TICKET_PRIORITY" != "high" ] && [ "$GIRA_TICKET_PRIORITY" != "critical" ]; then
exit 0
fi
# Different colors and channels based on priority
if [ "$GIRA_TICKET_PRIORITY" = "critical" ]; then
COLOR="danger"
CHANNEL="#alerts"
EMOJI="đ¨"
else
COLOR="warning"
CHANNEL="#development"
EMOJI="â ī¸"
fi
curl -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{
\"channel\": \"$CHANNEL\",
\"text\": \"$EMOJI $GIRA_TICKET_PRIORITY priority ticket created: $GIRA_TICKET_ID\",
\"attachments\": [{
\"color\": \"$COLOR\",
\"title\": \"$GIRA_TICKET_TITLE\",
\"fields\": [
{\"title\": \"Type\", \"value\": \"$GIRA_TICKET_TYPE\", \"short\": true},
{\"title\": \"Assignee\", \"value\": \"$GIRA_TICKET_ASSIGNEE\", \"short\": true}
]
}]
}"
Discord Notifications¶
#!/bin/bash
# .gira/hooks/ticket-moved.sh
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL}"
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
exit 0
fi
# Status change emoji mapping
case "$GIRA_NEW_STATUS" in
"todo") EMOJI="đ" ;;
"in_progress") EMOJI="đ" ;;
"review") EMOJI="đ" ;;
"done") EMOJI="â
" ;;
*) EMOJI="đ" ;;
esac
curl -X POST "$DISCORD_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{
\"embeds\": [{
\"title\": \"$EMOJI Ticket Status Changed\",
\"description\": \"**$GIRA_TICKET_ID**: $GIRA_TICKET_TITLE\",
\"color\": 3447003,
\"fields\": [
{\"name\": \"From\", \"value\": \"$GIRA_OLD_STATUS\", \"inline\": true},
{\"name\": \"To\", \"value\": \"$GIRA_NEW_STATUS\", \"inline\": true},
{\"name\": \"Assignee\", \"value\": \"$GIRA_TICKET_ASSIGNEE\", \"inline\": true}
]
}]
}"
Email Notifications¶
#!/usr/bin/env python3
# .gira/hooks/ticket-created.py
import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# Email configuration from environment
SMTP_HOST = os.environ.get('SMTP_HOST', 'localhost')
SMTP_PORT = int(os.environ.get('SMTP_PORT', '587'))
SMTP_USER = os.environ.get('SMTP_USER')
SMTP_PASS = os.environ.get('SMTP_PASS')
FROM_EMAIL = os.environ.get('FROM_EMAIL', 'gira@example.com')
TO_EMAIL = os.environ.get('TO_EMAIL', 'team@example.com')
if not all([SMTP_USER, SMTP_PASS, TO_EMAIL]):
print("Email configuration missing")
exit(0)
# Get ticket details
ticket_id = os.environ['GIRA_TICKET_ID']
ticket_title = os.environ['GIRA_TICKET_TITLE']
ticket_priority = os.environ['GIRA_TICKET_PRIORITY']
ticket_assignee = os.environ['GIRA_TICKET_ASSIGNEE']
# Only email for high priority tickets
if ticket_priority not in ['high', 'critical']:
exit(0)
# Create email
msg = MIMEMultipart()
msg['From'] = FROM_EMAIL
msg['To'] = TO_EMAIL
msg['Subject'] = f"đĢ New {ticket_priority} priority ticket: {ticket_id}"
body = f"""
A new {ticket_priority} priority ticket has been created:
Ticket: {ticket_id}
Title: {ticket_title}
Assignee: {ticket_assignee}
Priority: {ticket_priority}
Please review and take appropriate action.
"""
msg.attach(MIMEText(body, 'plain'))
# Send email
try:
server = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
server.starttls()
server.login(SMTP_USER, SMTP_PASS)
server.send_message(msg)
server.quit()
print(f"Email notification sent for {ticket_id}")
except Exception as e:
print(f"Failed to send email: {e}")
đ Automation¶
Auto-Assignment¶
#!/bin/bash
# .gira/hooks/ticket-created.sh
# Auto-assign tickets based on labels and type
assignee=""
# Bug assignment based on component
if [ "$GIRA_TICKET_TYPE" = "bug" ]; then
if [[ "$GIRA_TICKET_LABELS" == *"frontend"* ]]; then
assignee="frontend-dev@company.com"
elif [[ "$GIRA_TICKET_LABELS" == *"backend"* ]]; then
assignee="backend-dev@company.com"
elif [[ "$GIRA_TICKET_LABELS" == *"database"* ]]; then
assignee="dba@company.com"
fi
fi
# Feature assignment based on epic
if [ "$GIRA_TICKET_TYPE" = "feature" ] && [ -n "$GIRA_TICKET_EPIC_ID" ]; then
case "$GIRA_TICKET_EPIC_ID" in
"EPIC-AUTH") assignee="auth-team@company.com" ;;
"EPIC-API") assignee="api-team@company.com" ;;
esac
fi
# Update ticket if assignee determined
if [ -n "$assignee" ] && [ "$GIRA_TICKET_ASSIGNEE" != "$assignee" ]; then
echo "Auto-assigning $GIRA_TICKET_ID to $assignee"
cd "$GIRA_ROOT"
gira ticket update "$GIRA_TICKET_ID" --assignee "$assignee"
fi
Git Branch Creation¶
#!/usr/bin/env python3
# .gira/hooks/ticket-moved.py
import os
import subprocess
import re
# Only create branches when moving to in_progress
if os.environ.get('GIRA_NEW_STATUS') != 'in_progress':
exit(0)
ticket_id = os.environ['GIRA_TICKET_ID']
ticket_title = os.environ['GIRA_TICKET_TITLE']
ticket_type = os.environ['GIRA_TICKET_TYPE']
# Create branch name from ticket
# Convert title to kebab-case
title_clean = re.sub(r'[^\w\s-]', '', ticket_title)
title_clean = re.sub(r'[-\s]+', '-', title_clean).strip('-').lower()
branch_name = f"{ticket_type}/{ticket_id.lower()}-{title_clean}"
try:
# Check if we're in a git repository
subprocess.run(['git', 'rev-parse', '--git-dir'],
check=True, capture_output=True)
# Check if branch already exists
result = subprocess.run(['git', 'branch', '--list', branch_name],
capture_output=True, text=True)
if not result.stdout.strip():
# Create and switch to new branch
subprocess.run(['git', 'checkout', '-b', branch_name], check=True)
print(f"â
Created and switched to branch: {branch_name}")
# Optionally push to remote
try:
subprocess.run(['git', 'push', '-u', 'origin', branch_name],
check=True, capture_output=True)
print(f"đ¤ Pushed branch to remote: {branch_name}")
except subprocess.CalledProcessError:
print(f"â ī¸ Branch created locally, but couldn't push to remote")
else:
print(f"âšī¸ Branch already exists: {branch_name}")
except subprocess.CalledProcessError:
print("âšī¸ Not in a git repository or git command failed")
except Exception as e:
print(f"â Error creating branch: {e}")
Label-Based Actions¶
#!/bin/bash
# .gira/hooks/ticket-updated.sh
# Actions based on labels
labels="$GIRA_TICKET_LABELS"
# Documentation required
if [[ "$labels" == *"needs-docs"* ]]; then
echo "đ This ticket requires documentation updates"
# Create documentation ticket if it doesn't exist
docs_ticket_id="${GIRA_TICKET_ID}-docs"
if [ ! -f "$GIRA_ROOT/.gira/board/todo/${docs_ticket_id}.json" ]; then
cd "$GIRA_ROOT"
gira ticket create "Documentation for $GIRA_TICKET_ID" \
--type task \
--priority medium \
--labels "documentation" \
--description "Update documentation for ticket $GIRA_TICKET_ID: $GIRA_TICKET_TITLE"
echo "đ Created documentation ticket: ${docs_ticket_id}"
fi
fi
# Security review required
if [[ "$labels" == *"security-review"* ]]; then
echo "đ Security review required for $GIRA_TICKET_ID"
# Notify security team
if [ -n "$SECURITY_TEAM_EMAIL" ]; then
echo "Security review needed for $GIRA_TICKET_ID" | \
mail -s "Security Review: $GIRA_TICKET_ID" "$SECURITY_TEAM_EMAIL"
fi
fi
# Performance testing
if [[ "$labels" == *"perf-test"* ]]; then
echo "⥠Performance testing required for $GIRA_TICKET_ID"
# Could trigger automated performance tests
fi
đ Reporting & Analytics¶
Sprint Completion Report¶
#!/usr/bin/env python3
# .gira/hooks/sprint-completed.py
import os
import json
from datetime import datetime
from pathlib import Path
# Get sprint information
sprint_id = os.environ['GIRA_SPRINT_ID']
sprint_name = os.environ['GIRA_SPRINT_NAME']
sprint_tickets = os.environ.get('GIRA_SPRINT_TICKETS', '').split(',')
gira_root = Path(os.environ['GIRA_ROOT'])
print(f"đ Generating completion report for sprint: {sprint_name}")
# Analyze tickets
completed_tickets = []
incomplete_tickets = []
total_story_points = 0
completed_story_points = 0
for ticket_id in sprint_tickets:
if not ticket_id.strip():
continue
# Find ticket in any status
ticket_data = None
for status in ['done', 'in_progress', 'review', 'todo', 'backlog']:
ticket_path = gira_root / '.gira' / 'board' / status / f'{ticket_id.strip()}.json'
if not ticket_path.exists():
# Check backlog with hashed structure
if status == 'backlog':
hash_dir = ticket_id.strip()[:2].lower()
ticket_path = gira_root / '.gira' / 'board' / 'backlog' / hash_dir / f'{ticket_id.strip()}.json'
if ticket_path.exists():
try:
with open(ticket_path) as f:
ticket_data = json.load(f)
break
except Exception as e:
print(f"Error reading {ticket_id}: {e}")
if ticket_data:
story_points = ticket_data.get('story_points', 0) or 0
total_story_points += story_points
if ticket_data['status'] == 'done':
completed_tickets.append(ticket_data)
completed_story_points += story_points
else:
incomplete_tickets.append(ticket_data)
# Calculate metrics
completion_rate = (len(completed_tickets) / len([t for t in sprint_tickets if t.strip()])) * 100 if sprint_tickets else 0
velocity = completed_story_points
# Generate report
report_dir = gira_root / '.gira' / 'reports'
report_dir.mkdir(exist_ok=True)
report_file = report_dir / f'sprint-{sprint_id}-report.md'
with open(report_file, 'w') as f:
f.write(f"# Sprint Completion Report: {sprint_name}\n\n")
f.write(f"**Sprint ID:** {sprint_id}\n")
f.write(f"**Completed:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
f.write("## đ Summary\n\n")
f.write(f"- **Total Tickets:** {len([t for t in sprint_tickets if t.strip()])}\n")
f.write(f"- **Completed Tickets:** {len(completed_tickets)}\n")
f.write(f"- **Incomplete Tickets:** {len(incomplete_tickets)}\n")
f.write(f"- **Completion Rate:** {completion_rate:.1f}%\n")
f.write(f"- **Story Points Completed:** {completed_story_points}/{total_story_points}\n")
f.write(f"- **Velocity:** {velocity} story points\n\n")
if completed_tickets:
f.write("## â
Completed Tickets\n\n")
for ticket in completed_tickets:
sp = ticket.get('story_points') or 0
f.write(f"- **{ticket['id']}** ({sp} SP): {ticket['title']}\n")
if incomplete_tickets:
f.write(f"\n## đ Incomplete Tickets\n\n")
for ticket in incomplete_tickets:
sp = ticket.get('story_points') or 0
f.write(f"- **{ticket['id']}** ({sp} SP): {ticket['title']} (status: {ticket['status']})\n")
print(f"đ Sprint report saved to: {report_file}")
# Generate JSON summary for further processing
summary = {
'sprint_id': sprint_id,
'sprint_name': sprint_name,
'completion_date': datetime.now().isoformat(),
'metrics': {
'total_tickets': len([t for t in sprint_tickets if t.strip()]),
'completed_tickets': len(completed_tickets),
'completion_rate': completion_rate,
'total_story_points': total_story_points,
'completed_story_points': completed_story_points,
'velocity': velocity
}
}
with open(report_dir / f'sprint-{sprint_id}-summary.json', 'w') as f:
json.dump(summary, f, indent=2)
Daily Activity Log¶
#!/bin/bash
# .gira/hooks/ticket-moved.sh
# Log all ticket movements for daily standup reports
LOG_DIR="$GIRA_ROOT/.gira/logs"
mkdir -p "$LOG_DIR"
DAILY_LOG="$LOG_DIR/daily-$(date +%Y-%m-%d).log"
TIMESTAMP=$(date -Iseconds)
# Log the movement
echo "$TIMESTAMP,$GIRA_TICKET_ID,$GIRA_TICKET_TITLE,$GIRA_OLD_STATUS,$GIRA_NEW_STATUS,$GIRA_TICKET_ASSIGNEE" >> "$DAILY_LOG"
# Generate daily summary at end of day (if it's after 5 PM)
if [ "$(date +%H)" -ge 17 ]; then
SUMMARY_FILE="$LOG_DIR/daily-summary-$(date +%Y-%m-%d).md"
if [ ! -f "$SUMMARY_FILE" ]; then
echo "# Daily Summary - $(date +%Y-%m-%d)" > "$SUMMARY_FILE"
echo "" >> "$SUMMARY_FILE"
# Tickets moved to done today
echo "## â
Completed Today" >> "$SUMMARY_FILE"
grep ",done," "$DAILY_LOG" | while IFS=, read -r timestamp ticket_id title old_status new_status assignee; do
echo "- **$ticket_id**: $title (by $assignee)" >> "$SUMMARY_FILE"
done
# Tickets moved to in_progress today
echo "" >> "$SUMMARY_FILE"
echo "## đ Started Today" >> "$SUMMARY_FILE"
grep ",in_progress," "$DAILY_LOG" | while IFS=, read -r timestamp ticket_id title old_status new_status assignee; do
echo "- **$ticket_id**: $title (by $assignee)" >> "$SUMMARY_FILE"
done
echo "đ Daily summary generated: $SUMMARY_FILE"
fi
fi
đĄī¸ Governance & Validation¶
Ticket Validation¶
#!/usr/bin/env python3
# .gira/hooks/ticket-created.py
import os
import re
import sys
ticket_id = os.environ['GIRA_TICKET_ID']
ticket_title = os.environ['GIRA_TICKET_TITLE']
ticket_description = os.environ['GIRA_TICKET_DESCRIPTION']
ticket_type = os.environ['GIRA_TICKET_TYPE']
errors = []
warnings = []
# Title validation
if len(ticket_title) < 10:
errors.append("Title must be at least 10 characters long")
if len(ticket_title) > 100:
warnings.append("Title is quite long, consider shortening")
# Title format validation
if ticket_type == 'bug' and not re.match(r'^(Fix|Bug)', ticket_title, re.IGNORECASE):
warnings.append("Bug tickets should start with 'Fix' or 'Bug'")
if ticket_type == 'feature' and not re.match(r'^(Add|Implement|Create)', ticket_title, re.IGNORECASE):
warnings.append("Feature tickets should start with 'Add', 'Implement', or 'Create'")
# Description validation
if not ticket_description or len(ticket_description.strip()) < 20:
errors.append("Description must be at least 20 characters long")
# Bug-specific validation
if ticket_type == 'bug':
required_sections = ['## Steps to Reproduce', '## Expected Behavior', '## Actual Behavior']
for section in required_sections:
if section.lower() not in ticket_description.lower():
errors.append(f"Bug reports must include '{section}' section")
# Feature-specific validation
if ticket_type == 'feature':
if '## Acceptance Criteria' not in ticket_description:
warnings.append("Feature tickets should include '## Acceptance Criteria' section")
# Print results
if errors:
print("â Ticket validation errors:")
for error in errors:
print(f" - {error}")
print(f"\nPlease fix these issues in ticket {ticket_id}")
# Don't fail the ticket creation, just warn
if warnings:
print("â ī¸ Ticket validation warnings:")
for warning in warnings:
print(f" - {warning}")
if not errors and not warnings:
print(f"â
Ticket {ticket_id} passed validation")
Approval Workflow¶
#!/bin/bash
# .gira/hooks/ticket-moved.sh
# Require approval for certain transitions
if [ "$GIRA_NEW_STATUS" = "done" ]; then
# Check if this needs approval
needs_approval=false
# High priority tickets need approval
if [ "$GIRA_TICKET_PRIORITY" = "critical" ] || [ "$GIRA_TICKET_PRIORITY" = "high" ]; then
needs_approval=true
fi
# Security-related tickets need approval
if [[ "$GIRA_TICKET_LABELS" == *"security"* ]]; then
needs_approval=true
fi
# Production deployment tickets need approval
if [[ "$GIRA_TICKET_LABELS" == *"deployment"* ]]; then
needs_approval=true
fi
if [ "$needs_approval" = true ]; then
echo "â ī¸ Ticket $GIRA_TICKET_ID requires approval before completion"
# Move back to review status
cd "$GIRA_ROOT"
gira ticket move "$GIRA_TICKET_ID" review
# Add approval comment
echo "This ticket requires approval due to priority/labels. Please get approval before marking as done." | \
gira comment add "$GIRA_TICKET_ID" --content-file -
# Notify approvers
APPROVERS="${APPROVERS:-manager@company.com,lead@company.com}"
if [ -n "$APPROVERS" ]; then
echo "Ticket $GIRA_TICKET_ID requires your approval" | \
mail -s "Approval Required: $GIRA_TICKET_ID" "$APPROVERS"
fi
echo "đ§ Approval request sent"
fi
fi
đ External Integrations¶
Jira Synchronization¶
#!/usr/bin/env python3
# .gira/hooks/ticket-updated.py
import os
import requests
import json
from base64 import b64encode
# Jira configuration
JIRA_URL = os.environ.get('JIRA_URL')
JIRA_USER = os.environ.get('JIRA_USER')
JIRA_TOKEN = os.environ.get('JIRA_TOKEN')
JIRA_PROJECT_KEY = os.environ.get('JIRA_PROJECT_KEY', 'PROJ')
if not all([JIRA_URL, JIRA_USER, JIRA_TOKEN]):
print("Jira integration not configured")
exit(0)
# Get ticket info
ticket_id = os.environ['GIRA_TICKET_ID']
ticket_title = os.environ['GIRA_TICKET_TITLE']
ticket_status = os.environ['GIRA_TICKET_STATUS']
ticket_assignee = os.environ['GIRA_TICKET_ASSIGNEE']
# Map Gira status to Jira status
status_mapping = {
'todo': 'To Do',
'in_progress': 'In Progress',
'review': 'In Review',
'done': 'Done'
}
jira_status = status_mapping.get(ticket_status, ticket_status)
# Create Jira issue key from Gira ticket ID
jira_key = f"{JIRA_PROJECT_KEY}-{ticket_id.split('-')[-1]}"
# Setup authentication
auth_string = f"{JIRA_USER}:{JIRA_TOKEN}"
auth_b64 = b64encode(auth_string.encode()).decode()
headers = {
'Authorization': f'Basic {auth_b64}',
'Content-Type': 'application/json'
}
try:
# Check if issue exists in Jira
response = requests.get(
f"{JIRA_URL}/rest/api/2/issue/{jira_key}",
headers=headers,
timeout=10
)
if response.status_code == 200:
# Update existing issue
update_data = {
"fields": {
"summary": ticket_title,
"assignee": {"emailAddress": ticket_assignee} if ticket_assignee else None
}
}
# Update status if needed
# Note: This requires knowledge of Jira workflow transitions
# You might need to customize this based on your Jira setup
response = requests.put(
f"{JIRA_URL}/rest/api/2/issue/{jira_key}",
headers=headers,
json=update_data,
timeout=10
)
if response.status_code == 204:
print(f"â
Updated Jira issue: {jira_key}")
else:
print(f"â ī¸ Failed to update Jira issue: {response.status_code}")
elif response.status_code == 404:
print(f"âšī¸ Jira issue {jira_key} not found (not syncing)")
else:
print(f"â ī¸ Error checking Jira issue: {response.status_code}")
except Exception as e:
print(f"â Error syncing with Jira: {e}")
Time Tracking Integration¶
#!/bin/bash
# .gira/hooks/ticket-moved.sh
# Track time when tickets move to/from in_progress
TIME_TRACKING_API="${TIME_TRACKING_API}"
API_KEY="${TIME_TRACKING_API_KEY}"
if [ -z "$TIME_TRACKING_API" ] || [ -z "$API_KEY" ]; then
exit 0
fi
# Start time tracking when moving to in_progress
if [ "$GIRA_NEW_STATUS" = "in_progress" ] && [ "$GIRA_OLD_STATUS" != "in_progress" ]; then
echo "âąī¸ Starting time tracking for $GIRA_TICKET_ID"
curl -X POST "$TIME_TRACKING_API/start" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"ticket_id\": \"$GIRA_TICKET_ID\",
\"title\": \"$GIRA_TICKET_TITLE\",
\"assignee\": \"$GIRA_TICKET_ASSIGNEE\",
\"started_at\": \"$(date -Iseconds)\"
}"
fi
# Stop time tracking when moving away from in_progress
if [ "$GIRA_OLD_STATUS" = "in_progress" ] && [ "$GIRA_NEW_STATUS" != "in_progress" ]; then
echo "âšī¸ Stopping time tracking for $GIRA_TICKET_ID"
curl -X POST "$TIME_TRACKING_API/stop" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"ticket_id\": \"$GIRA_TICKET_ID\",
\"stopped_at\": \"$(date -Iseconds)\",
\"final_status\": \"$GIRA_NEW_STATUS\"
}"
fi
đī¸ Development Workflow¶
CI/CD Integration¶
#!/bin/bash
# .gira/hooks/ticket-moved.sh
# Trigger CI/CD pipeline when ticket moves to review
if [ "$GIRA_NEW_STATUS" = "review" ]; then
echo "đ Triggering CI/CD pipeline for $GIRA_TICKET_ID"
# GitHub Actions workflow dispatch
if [ -n "$GITHUB_TOKEN" ] && [ -n "$GITHUB_REPO" ]; then
curl -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/$GITHUB_REPO/actions/workflows/review.yml/dispatches" \
-d "{\"ref\":\"main\",\"inputs\":{\"ticket_id\":\"$GIRA_TICKET_ID\"}}"
fi
# Jenkins build trigger
if [ -n "$JENKINS_URL" ] && [ -n "$JENKINS_TOKEN" ]; then
curl -X POST "$JENKINS_URL/job/review-pipeline/buildWithParameters" \
--user "$JENKINS_USER:$JENKINS_TOKEN" \
--data "TICKET_ID=$GIRA_TICKET_ID&BRANCH=feature/$GIRA_TICKET_ID"
fi
fi
Code Quality Gates¶
#!/usr/bin/env python3
# .gira/hooks/ticket-moved.py
import os
import subprocess
import requests
# Only run quality checks when moving to review
if os.environ.get('GIRA_NEW_STATUS') != 'review':
exit(0)
ticket_id = os.environ['GIRA_TICKET_ID']
gira_root = os.environ['GIRA_ROOT']
print(f"đ Running quality checks for {ticket_id}")
# Run linting
try:
result = subprocess.run(['flake8', '.'], capture_output=True, text=True, cwd=gira_root)
if result.returncode != 0:
print("â Linting failed:")
print(result.stdout)
# Add comment to ticket
subprocess.run([
'gira', 'comment', 'add', ticket_id,
'--content', f"â Linting failed. Please fix issues before review:\n```\n{result.stdout}\n```"
], cwd=gira_root)
# Move back to in_progress
subprocess.run(['gira', 'ticket', 'move', ticket_id, 'in_progress'], cwd=gira_root)
exit(1)
else:
print("â
Linting passed")
except Exception as e:
print(f"â ī¸ Could not run linting: {e}")
# Run tests
try:
result = subprocess.run(['pytest', '--tb=short'], capture_output=True, text=True, cwd=gira_root)
if result.returncode != 0:
print("â Tests failed:")
print(result.stdout)
# Add comment to ticket
subprocess.run([
'gira', 'comment', 'add', ticket_id,
'--content', f"â Tests failed. Please fix failing tests:\n```\n{result.stdout}\n```"
], cwd=gira_root)
# Move back to in_progress
subprocess.run(['gira', 'ticket', 'move', ticket_id, 'in_progress'], cwd=gira_root)
exit(1)
else:
print("â
All tests passed")
except Exception as e:
print(f"â ī¸ Could not run tests: {e}")
# Security scan (example with bandit for Python)
try:
result = subprocess.run(['bandit', '-r', '.', '-f', 'json'], capture_output=True, text=True, cwd=gira_root)
if result.returncode != 0:
print("â ī¸ Security issues found")
# Parse JSON output and add to ticket comment
# This is a simplified example
subprocess.run([
'gira', 'comment', 'add', ticket_id,
'--content', "â ī¸ Security scan found potential issues. Please review."
], cwd=gira_root)
else:
print("đ Security scan passed")
except Exception as e:
print(f"âšī¸ Security scan not available: {e}")
print(f"â
Quality checks completed for {ticket_id}")
Deployment Automation¶
#!/bin/bash
# .gira/hooks/ticket-moved.sh
# Auto-deploy when tickets with deployment label move to done
if [ "$GIRA_NEW_STATUS" = "done" ] && [[ "$GIRA_TICKET_LABELS" == *"auto-deploy"* ]]; then
echo "đ Auto-deploying changes for $GIRA_TICKET_ID"
# Determine deployment environment based on labels
if [[ "$GIRA_TICKET_LABELS" == *"hotfix"* ]]; then
ENVIRONMENT="production"
elif [[ "$GIRA_TICKET_LABELS" == *"staging"* ]]; then
ENVIRONMENT="staging"
else
ENVIRONMENT="development"
fi
echo "đĻ Deploying to $ENVIRONMENT environment"
# Trigger deployment based on your deployment system
# Example with kubectl
if [ "$ENVIRONMENT" = "production" ]; then
kubectl set image deployment/app app=myapp:$GIRA_TICKET_ID -n production
elif [ "$ENVIRONMENT" = "staging" ]; then
kubectl set image deployment/app app=myapp:$GIRA_TICKET_ID -n staging
fi
# Log deployment
echo "$(date -Iseconds): Deployed $GIRA_TICKET_ID to $ENVIRONMENT" >> "$GIRA_ROOT/.gira/logs/deployments.log"
# Add deployment comment to ticket
cd "$GIRA_ROOT"
echo "đ Automatically deployed to $ENVIRONMENT environment at $(date)" | \
gira comment add "$GIRA_TICKET_ID" --content-file -
echo "â
Deployment completed for $GIRA_TICKET_ID"
fi
đ§ Setup Instructions¶
Quick Setup¶
- Initialize hooks:
gira ext init
- Choose examples: Copy examples from this guide to
.gira/hooks/
- Configure: Set up environment variables for integrations
- Test:
gira ext test <hook-name>
- Enable:
gira ext enable
Environment Variables¶
Set these in your shell profile (.bashrc
, .zshrc
, etc.):
# Slack integration
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
# Email notifications
export SMTP_HOST="smtp.gmail.com"
export SMTP_PORT="587"
export SMTP_USER="your-email@gmail.com"
export SMTP_PASS="your-app-password"
export TO_EMAIL="team@company.com"
# Jira integration
export JIRA_URL="https://company.atlassian.net"
export JIRA_USER="your-email@company.com"
export JIRA_TOKEN="your-api-token"
export JIRA_PROJECT_KEY="PROJ"
# GitHub integration
export GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
export GITHUB_REPO="owner/repository"
# Time tracking
export TIME_TRACKING_API="https://api.timetracker.com"
export TIME_TRACKING_API_KEY="your-api-key"
Testing Examples¶
# Test notification hooks
gira ext test ticket-created --data '{"ticket_priority": "critical"}'
# Test automation hooks
gira ext test ticket-moved --data '{"old_status": "todo", "new_status": "in_progress"}'
# Test with your own data
gira ext test ticket-created --data '{
"ticket_id": "TEST-123",
"ticket_title": "Test Integration",
"ticket_priority": "high",
"ticket_type": "bug",
"ticket_assignee": "dev@company.com"
}'
These examples provide a solid foundation for automating your Gira workflows. Mix and match them to create the perfect setup for your team's needs!
For more advanced customization, see the Hook System User Guide.