Skip to content

Hook Examples

Copy-paste hook examples for common automation and integration scenarios.

📖 Table of Contents


🔔 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

  1. Initialize hooks: gira ext init
  2. Choose examples: Copy examples from this guide to .gira/hooks/
  3. Configure: Set up environment variables for integrations
  4. Test: gira ext test <hook-name>
  5. 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.