DevOps Automation: From GitHub Issue to Jira Ticket with Python and Flask

Bridging the gap between development and project management with a simple, automated solution.

Created by Ron-Tino on March 18, 2025
AWS Lambda

In the fast-paced world of software development, efficiency is key. DevOps practices emphasize automation to streamline workflows, reduce manual effort, and improve collaboration. This post walks through a practical example of DevOps automation: integrating GitHub issues with Jira using a custom-built Python API, Flask, and webhooks. We'll see how a little bit of code can bridge the gap between development and project management, making life easier for everyone.

The Problem: Manual Handoffs are Slow

Imagine a scenario: Developers are working on a project hosted on GitHub. Testers (or other developers) find bugs and create issues on GitHub. A developer reviews the issue and decides it needs to be tracked in Jira (a popular project management tool). Manually creating a Jira ticket for every valid GitHub issue is tedious, error-prone, and time-consuming. This manual handoff creates a bottleneck.

The Solution: Automation to the Rescue!

Our goal is to build a system where a simple comment on a GitHub issue ("/jira", in our example, but you can customize this) automatically creates a corresponding Jira ticket. This eliminates the manual copying and pasting, ensures consistency, and keeps everyone on the same page.

The Tools of the Trade

We'll use a combination of tools to achieve this:

  • GitHub: The code repository and issue tracker.
  • Jira: The project management tool where we'll create tickets.
  • Python: Our programming language of choice.
  • Flask: A lightweight Python web framework for building our API. Think of Flask as a toolkit for making web applications.
  • AWS EC2: A virtual server (an instance) in the cloud where we'll deploy our Python application. You can use your local machine, but a cloud server like EC2 is good practice.
  • Webhooks: The magic that connects GitHub to our Python application. A webhook is like a notification system: when something happens on GitHub (a comment is made), GitHub sends a message to our application.

Part 1: The Jira Connection (Review)

In a previous post/video, we covered the basics of interacting with the Jira API using Python. This forms the foundation of our automation. If you haven't already, I recommend checking out that material to get familiar with the fundamentals of using the Jira API.

Part 2: Building the API with Flask

The heart of our solution is a Python API that acts as the intermediary between GitHub and Jira. We'll use Flask to build this API. Flask makes it surprisingly easy to create web APIs in Python. It handles the low-level details of receiving requests and sending responses, so we can focus on the core logic.

Why an API?

GitHub can't directly execute a Python script on your computer or a server. An API provides a standardized way for different applications to communicate. Think of it like this:

  • Without an API: It's like trying to give someone instructions by shouting across a crowded room. They might not hear you, and even if they do, they might not understand.
  • With an API: It's like having a phone call. You have a clear, defined way to communicate, and you can be sure the other person receives your message.

Our Starting Point (Initial Flask Code)

Let's begin with a basic Flask application structure. This code provides the foundation, but we'll need to make several crucialimprovements to make it a functional and robust solution.

                        
from flask import Flask
import requests
from requests.auth import HTTPBasicAuth
import json

# Create a Flask application instance
app = Flask(__name__)

@app.route("/createJIRA", methods=['POST'])  # Only accept POST requests to /createJIRA
def createJIRA():
    # This code sample uses the 'requests' library:
    # http://docs.python-requests.org

    # Corrected URL -  MAKE SURE TO UPDATE THIS WITH YOUR INSTANCE
    url = "https://your-instance.atlassian.net/rest/api/3/issue"
    API_TOKEN = "YOUR_API_TOKEN"  #  Replace with your ACTUAL API token

    auth = HTTPBasicAuth("your-email@example.com", API_TOKEN) # Replace with your email

    headers = {
        "Accept": "application/json",
        "Content-Type": "application/json"
    }

    payload = json.dumps({  # The Jira issue data
        "fields": {
            "description": {
                "content": [
                    {
                        "content": [
                            {
                                "text": "This the first ticket from the API",
                                "type": "text"
                            }
                        ],
                        "type": "paragraph"
                    }
                ],
                "type": "doc",
                "version": 1
            },
            "issuetype": {
                "id": "10003"  #  Replace with your desired issue type ID
            },
            "project": {
                "key": "AUT"  # Replace with your Jira project key!
            },
            "summary": "This is from Api automation",
        },
        "update": {}
    })

    response = requests.request(
        "POST",
        url,
        data=payload,
        headers=headers,
        auth=auth
    )

    return json.dumps(json.loads(response.text), sort_keys=True, indent=4, separators=(",", ": "))

app.run('0.0.0.0', port=5000)  # Start the Flask development server

                         

Essential Improvements: Making it Robust and Dynamic

While the above code provides a basic framework, it's missing several critical pieces. Here's what we need to add and why:

  1. Getting Data from GitHub (request): The original code doesn't actually receiveany data from GitHub! We need to use the `request` object from Flask to access the JSON payload sent by the GitHub webhook. This payload contains all the information about the issue and the comment that triggered the webhook. We'll add `from flask import request, jsonify` to our imports.
  2. Dynamic Data, Not Hardcoding: We need to extractthe relevant information (like the comment text, the GitHub issue URL, and the username of the commenter) from the GitHub payload. Hardcoding the Jira issue details makes the API useless for real-world scenarios.
  3. Trigger Condition (/jira): We need to add a check to ensure that the Jira ticket is only created when a specific comment is made (e.g., "`/jira`"). Otherwise, everycomment on everyissue would create a new ticket!
  4. Error Handling: What happens if the Jira API call fails? What if the GitHub payload is missing some data? We need to handle these cases gracefully.
  5. Security - Environment Variables: Hardcoding your API token and Jira URL directly in the code is a majorsecurity risk. We'll use environment variablesto store this sensitive information.

Here's the improvedFlask code, incorporating these crucial changes:

                            
from flask import Flask, request, jsonify  # Import request and jsonify
import requests
from requests.auth import HTTPBasicAuth
import json
import os  # Import os for environment variables

app = Flask(__name__)

# --- Use Environment Variables ---
JIRA_URL = os.environ.get("JIRA_URL", "https://your-instance.atlassian.net")  # Default, but use env var
API_TOKEN = os.environ.get("JIRA_API_TOKEN")
JIRA_EMAIL = os.environ.get("JIRA_EMAIL")

@app.route("/createJIRA", methods=['POST'])
def createJIRA():
    # --- Get data from the request (GitHub webhook) ---
    data = request.get_json()  # Get the JSON payload

    # --- Extract relevant information (EXAMPLE - adjust as needed) ---
    try:
        comment_body = data['comment']['body']  # Get comment from payload
        issue_url = data['issue']['html_url']  # Get issue URL
        user = data['comment']['user']['login']   # Get user
    except KeyError as e:
        return jsonify({"error": f"Missing key in payload: {e}"}), 400  # Handle missing data

    # --- Trigger Condition ---
    if comment_body.strip().lower() != "/jira":  # Check for trigger comment
        return jsonify({"message": "Not a Jira creation request."}), 200

    # --- Prepare Jira Issue Data (DYNAMIC, not hardcoded) ---
    payload = json.dumps({
        "fields": {
            "description": {
                "content": [
                    {
                        "content": [
                            {
                                "text": f"Issue created from GitHub: {issue_url}\nComment: {comment_body}\nBy user: {user}",
                                "type": "text"
                            }
                        ],
                        "type": "paragraph"
                    }
                ],
                "type": "doc",
                "version": 1
            },
            "issuetype": {
                "id": "10003"  # Still potentially hardcoded - consider making this configurable
            },
            "project": {
                "key": "AUT"  # Still hardcoded - consider making this configurable
            },
            "summary": f"GitHub Issue: {issue_url.split('/')[-1]}", # Dynamic summary
        },
        "update": {}
    })

    # --- Make the API Request ---
    auth = HTTPBasicAuth(JIRA_EMAIL, API_TOKEN)
    headers = {
        "Accept": "application/json",
        "Content-Type": "application/json"
    }

    response = requests.post(
        f"{JIRA_URL}/rest/api/3/issue",  # Use f-string for URL
        data=payload,
        headers=headers,
        auth=auth
    )

    # --- Handle the Response (with error checking) ---
    if response.status_code == 201:  # 201 Created
        return jsonify({"message": "Jira issue created!", "key": response.json()["key"]}), 201
    else:
        return jsonify({"error": "Failed to create Jira issue", "status_code": response.status_code, "response": response.text}), response.status_code

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=5000, debug=True)  # debug=True for development

Code Breakdown (Improved Version):

  • from flask import request, jsonify: We import request to access incoming data and jsonify to create JSON responses, making our API more robust.
  • Environment Variables: We use os.environ.get() to load sensitive information (Jira URL, API token, email) from environment variables. This is a criticalsecurity best practice. Never hardcode credentials!
  • data = request.get_json(): This line is the key to receiving data from GitHub. The webhook sends a JSON payload, and this line extracts it.
  • Data Extraction and try...except: We extract the comment body, issue URL, and user from the data dictionary. The try...except KeyError block is essentialfor handling cases where the GitHub payload might be different from what we expect. This prevents the application from crashing.
  • Trigger Condition: The if comment_body.strip().lower() != "/jira": line checks if the comment contains our trigger phrase ("/jira"). If it doesn't, we return a 200 OK response (so GitHub knows the webhook was received), but we don't create a Jira ticket.
  • Dynamic Payload: The payload sent to Jira is now dynamic. We use f-strings (e.g., f"Issue created from GitHub: {issue_url}") to insert the relevant information from the GitHub issue into the Jira ticket description.
  • Robust Error Handling: We check the response.status_code from the Jira API. If it's not 201 (Created), we return an error message, including the status code and the response text from Jira. This helps with debugging.
  • `response.json()`: We use the more reliable `response.json()` method to parse the JSON response from Jira.

Important: You'll need to adapt the data extraction part (data['comment']['body'], etc.) to match the actual structure of the GitHub webhook payload. Use print(json.dumps(data, indent=4)) (as shown in the commented-out line in the code) to inspect the payload and see the exact keys you need to access.

Part 3: Deploying to EC2 (or your local machine)

  1. Create an EC2 Instance (if needed):
    • Launch an EC2 instance (e.g., a t2.micro running Ubuntu).
    • Make sure the security group allows inbound traffic on port 5000 (for our Flask app).
    • Connect to the instance via SSH.
  2. Install Dependencies:
  3. Transfer the Code: Copy your Python file (e.g., `github_jira_integration.py`) to the EC2 instance. You can use `scp`, `rsync`, or even copy and paste the code into a new file using a text editor like `nano` or `vim`.
    • Replace /path/to/your/key.pem with the actual path to your private key.
    • Replace your_ec2_public_ip with the public IP address of your EC2 instance.
  4. Set Environment Variables:
    • Important: Replace these with your actual values. For a more permanent solution, add these export commands to your ~/.bashrc or ~/.bash_profile file.
  5. Run the Application:

Your Flask application is now running and accessible on the EC2 instance's public IP address and port 5000.

Part 4: Setting Up the GitHub Webhook

  1. Go to your GitHub repository settings.
  2. Click on "Webhooks" in the sidebar.
  3. Click "Add webhook."
  4. Payload URL: Enter the URL of your Flask API endpoint. This will be something like: http://your_ec2_public_ip:5000/createJIRA (Replace your_ec2_public_ip with your EC2 instance's public IP address or DNS name).
  5. Content type: Select application/json.
  6. Secret: (Optional, but recommended for security) You can set a secret token here. Your Flask application would then need to verify this token to ensure the request is genuinely from GitHub.
  7. Which events would you like to trigger this webhook?
    • Choose "Let me select individual events."
    • Select "Issue comments." This is crucial.
  8. Active: Make sure the webhook is active.
  9. Click "Add webhook."

Part 5: Testing and Troubleshooting

  1. Go to a GitHub issue in your repository.
  2. Add a comment containing /jira (or whatever trigger phrase you chose).
  3. Check your Jira project. A new issue should be created.
  4. Check your Flask application logs (on the EC2 instance or your local machine) for any error messages.

Important Considerations and Improvements

  • Security:
    • HTTPS: Use HTTPS for your API endpoint. This requires setting up an SSL certificate (e.g., using Let's Encrypt).
    • Webhook Secret: Use a webhook secret to verify that requests are coming from GitHub.
    • Input Validation: Validate the data received from GitHub to prevent malicious input.
  • Production Deployment: The Flask development server (app.run()) is notsuitable for production. For a production deployment, you should use a production-ready WSGI server like Gunicorn or uWSGI, along with a web server like Nginx or Apache.
  • Asynchronous Processing: For long-running tasks (like creating Jira issues), consider using a task queue like Celery to avoid blocking the main API thread.
  • Logging: Implement proper logging to help with debugging and monitoring.

Conclusion

By combining the power of Python, Flask, GitHub webhooks, and a touch of DevOps thinking, we've created a valuable automation that streamlines the workflow between developers and project managers. This project demonstrates how a relatively small amount of code can have a big impact on efficiency and collaboration. Remember to adapt the code and configurations to your specific needs, and always prioritize security and robust error handling. Happy automating!