Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/create-jira-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Create Jira Issue

on:
workflow_call:
inputs:
story_summary:
required: true
type: string
story_description:
required: false
type: string
default: ""
subtask_summary:
required: true
type: string
subtask_description:
required: false
type: string
default: ""
environment:
required: true
type: string
secrets:
MIGRATION_BACKLOG_JIRA_EMAIL:
required: true
MIGRATION_BACKLOG_JIRA_TOKEN:
required: true
MIGRATION_BACKLOG_JIRA_URL:
required: true
MIGRATION_BACKLOG_JIRA_PROJECT:
required: true
MIGRATION_BACKLOG_JIRA_EPIC_KEY:
required: true
MIGRATION_BACKLOG_JIRA_EPIC_LINK_FIELD:
required: true

jobs:
create_jira_issue:
environment:
name: ${{ inputs.environment }}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of select the environment?

runs-on: ubuntu-latest
steps:
- name: Checkout actions-hub repo
uses: actions/checkout@v4
with:
repository: nelc/actions-hub
ref: and/jira_automation_action
path: actions-hub
- name: Debug input and env values
run: |
echo "Story Summary: ${{ inputs.story_summary }}"
echo "Subtask Summary: ${{ inputs.subtask_summary }}"
echo "Project: ${{ secrets.MIGRATION_BACKLOG_JIRA_PROJECT }}"
echo "Epic Key: ${{ secrets.MIGRATION_BACKLOG_JIRA_EPIC_KEY }}"
echo "Epic Field: ${{ secrets.MIGRATION_BACKLOG_JIRA_EPIC_LINK_FIELD }}"

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install dependencies
run: pip install jira

- name: Run Jira issue script
run: |
python actions-hub/scripts/jira_issue_flow.py \
--story-summary "${{ inputs.story_summary }}" \
--story-description "${{ inputs.story_description }}" \
--subtask-summary "${{ inputs.subtask_summary }}" \
--subtask-description "${{ inputs.subtask_description }}"
env:
MIGRATION_BACKLOG_JIRA_EMAIL: ${{ secrets.MIGRATION_BACKLOG_JIRA_EMAIL }}
MIGRATION_BACKLOG_JIRA_TOKEN: ${{ secrets.MIGRATION_BACKLOG_JIRA_TOKEN }}
MIGRATION_BACKLOG_JIRA_URL: ${{ secrets.MIGRATION_BACKLOG_JIRA_URL }}
MIGRATION_BACKLOG_JIRA_PROJECT: ${{ secrets.MIGRATION_BACKLOG_JIRA_PROJECT }}
MIGRATION_BACKLOG_JIRA_EPIC_KEY: ${{ secrets.MIGRATION_BACKLOG_JIRA_EPIC_KEY }}
MIGRATION_BACKLOG_JIRA_EPIC_LINK_FIELD: ${{ secrets.MIGRATION_BACKLOG_JIRA_EPIC_LINK_FIELD }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ yarn-error.log*
# System Files
.DS_Store
Thumbs.db

## virtual environments
venv
175 changes: 175 additions & 0 deletions scripts/jira_issue_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import argparse
import os
from functools import lru_cache
from typing import Optional

from jira import JIRA
from jira.resources import Issue


@lru_cache(maxsize=1)
def get_jira_client() -> JIRA:
"""
Returns a cached Jira client instance authenticated with the environment variables.

Returns:
JiraClient: An authenticated JIRA client instance.

Raises:
KeyError: If required environment variables are missing.
"""
email = os.environ["MIGRATION_BACKLOG_JIRA_EMAIL"]
token = os.environ["MIGRATION_BACKLOG_JIRA_TOKEN"]
url = os.environ["MIGRATION_BACKLOG_JIRA_URL"]

return JIRA(server=url, basic_auth=(email, token))


def get_jira_issue_by_jql(jql: str, summary: str) -> Optional[Issue]:
"""
Searches for a Jira issue using a JQL query with an exact summary match.

Args:
jql (str): The JQL query string.
summary (str): The exact summary of the issue to find.

Returns:
Optional[Issue]: The matching Jira issue, or None if not found.

Raises:
KeyError: If required environment variables are missing.
"""
issues = get_jira_client().search_issues(jql, maxResults=10)
for issue in issues:
if issue.fields.summary == summary:
return issue

return None


def create_jira_issue(summary: str, issue_type: str, parent_key: str, description: str = "") -> Issue:
"""
Creates a Jira issue (Story or Sub-task) in the configured project.

Args:
summary (str): The summary of the new issue.
issue_type (str): The type of issue to create (e.g., "Story", "Sub-task").
parent_key (str): The key of the Epic (for Story) or Story (for Sub-task).
description (str): The description of the issue.

Returns:
Issue: The newly created Jira issue.

Raises:
KeyError: If required environment variables are missing.
"""
project = os.environ["MIGRATION_BACKLOG_JIRA_PROJECT"]
epic_link_field = os.environ["MIGRATION_BACKLOG_JIRA_EPIC_LINK_FIELD"]
fields = {
"project": {"key": project},
"summary": summary,
"issuetype": {"name": issue_type},
"description": description,
}

if issue_type == "Story":
fields[epic_link_field] = parent_key
elif issue_type == "Sub-task":
fields["parent"] = {"key": parent_key}

return get_jira_client().create_issue(fields=fields)


def get_or_create_jira_issue(summary: str, jql: str, issue_type: str, parent_key, description: str = "") -> Issue:
"""
Retrieves a Jira issue using a JQL query with an exact summary match, or creates it if not found.

Args:
summary (str): The summary of the issue.
jql (str): The JQL query to search for the issue.
issue_type (str): The type of issue to create if not found.
parent_key (Optional[str]): The parent key (Epic or Story) if the issue needs to be linked.
description (str): The description of the issue.

Returns:
Issue: The existing or newly created Jira issue.

Raises:
KeyError: If required environment variables are missing.
"""
if issue := get_jira_issue_by_jql(jql=jql, summary=summary):
return issue

return create_jira_issue(summary=summary, issue_type=issue_type, parent_key=parent_key, description=description)


def get_or_create_story(summary: str, epic_key: str, description: str = "") -> Issue:
"""
Retrieves or creates a Jira Story linked to the specified Epic.

Args:
summary (str): The summary of the Story.
epic_key (str): The key of the Epic.
description (str): The description of the Story.

Returns:
Issue: The existing or newly created Story issue.
"""
project = os.environ["MIGRATION_BACKLOG_JIRA_PROJECT"]
jql = f'project = {project} AND summary ~ "{summary}" AND issuetype = Story AND "Epic Link" = "{epic_key}"'

return get_or_create_jira_issue(
summary=summary,
jql=jql,
issue_type="Story",
parent_key=epic_key,
description=description,
)


def get_or_create_subtask(summary: str, story_key: str, description: str = "") -> Issue:
"""
Retrieves or creates a Jira Sub-task under the specified Story.

Args:
summary (str): The summary of the Sub-task.
story_key (str): The key of the parent Story.
description (str): The description of the Sub-task.

Returns:
Issue: The existing or newly created Sub-task issue.
"""
project = os.environ["MIGRATION_BACKLOG_JIRA_PROJECT"]
jql = f'project = {project} AND summary ~ "{summary}" AND issuetype = Sub-task AND parent = "{story_key}"'

return get_or_create_jira_issue(
summary=summary,
jql=jql,
issue_type="Sub-task",
parent_key=story_key,
description=description,
)


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Create Jira Story and Sub-task if not present.")
parser.add_argument("--story-summary", required=True, help="Summary for the story.")
parser.add_argument("--story-description", default="", help="Description for the story.")
parser.add_argument("--subtask-summary", required=True, help="Summary for the sub-task.")
parser.add_argument("--subtask-description", default="", help="Description for the sub-task.")
args = parser.parse_args()

epic_key = os.environ["MIGRATION_BACKLOG_JIRA_EPIC_KEY"]

story = get_or_create_story(
summary=args.story_summary,
epic_key=epic_key,
description=args.story_description,
)
get_or_create_subtask(
summary=args.subtask_summary,
story_key=story.key,
description=args.subtask_description,
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add some logging indicating the success of the story or subtask created...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done
image


print(f"Jira story '{story.fields.summary}' and sub-task '{args.subtask_summary}' created or already exist.")
Loading