Skip to content
393 changes: 393 additions & 0 deletions .github/workflows/jira-ticket-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,393 @@
name: Jira Ticket Check
on:
pull_request:
types: [opened, synchronize, reopened, edited]
workflow_call:
inputs:
jira-project-pattern:
required: false
type: string
description: 'Regex pattern for Jira project keys (e.g., "DEVEX|DEV|PROJ")'
default: '[A-Z]+'
check-commits:
required: false
type: boolean
description: 'Whether to check commit messages for Jira tickets'
default: true
fail-on-missing:
required: false
type: boolean
description: 'Whether to fail the workflow if no Jira ticket is found'
default: true
verify-ticket-exists:
required: false
type: boolean
description: 'Whether to verify the ticket exists in Jira via API'
default: true
jira-instance:
required: false
type: string
description: 'Jira instance URL (e.g., revolutionparts.atlassian.net)'
default: 'revolutionparts.atlassian.net'
secrets:
JIRA_GITHUB_USER_EMAIL:
required: false
description: 'Jira account email for API authentication'
JIRA_GITHUB_USER_API_TOKEN:
required: false
description: 'Jira API token for authentication'

jobs:
check-jira-ticket:
name: Verify Jira Ticket Link
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Extract Jira ticket from branch name
id: branch-check
run: |
BRANCH_NAME="${{ github.head_ref }}"
JIRA_PROJECT_PATTERN="${{ inputs.jira-project-pattern }}"
if [ -z "$JIRA_PROJECT_PATTERN" ]; then
JIRA_PROJECT_PATTERN="[A-Za-z]+"
else
# Make pattern case-insensitive by adding both cases if it's a simple pattern
# For complex patterns, rely on grep -i flag
JIRA_PROJECT_PATTERN=$(echo "$JIRA_PROJECT_PATTERN" | sed 's/\[A-Z\]\+/[A-Za-z]+/g' | sed 's/\[A-Z\]/[A-Za-z]/g')
fi
JIRA_PATTERN="${JIRA_PROJECT_PATTERN}-[0-9]+"

if [ -z "$BRANCH_NAME" ]; then
echo "ticket=" >> $GITHUB_OUTPUT
echo "found_in=" >> $GITHUB_OUTPUT
exit 0
fi

# Case-insensitive search
if echo "$BRANCH_NAME" | grep -qiE "$JIRA_PATTERN"; then
TICKET=$(echo "$BRANCH_NAME" | grep -oiE "$JIRA_PATTERN" | head -1)
# Normalize to uppercase for Jira API (Jira ticket keys are uppercase)
TICKET=$(echo "$TICKET" | tr '[:lower:]' '[:upper:]')
echo "ticket=$TICKET" >> $GITHUB_OUTPUT
echo "found_in=branch" >> $GITHUB_OUTPUT
echo "✅ Found Jira ticket in branch name: $TICKET"
exit 0
fi

echo "ticket=" >> $GITHUB_OUTPUT
echo "found_in=" >> $GITHUB_OUTPUT

- name: Extract Jira ticket from PR title
id: pr-title-check
if: steps.branch-check.outputs.ticket == ''
run: |
PR_TITLE="${{ github.event.pull_request.title }}"
JIRA_PROJECT_PATTERN="${{ inputs.jira-project-pattern }}"
if [ -z "$JIRA_PROJECT_PATTERN" ]; then
JIRA_PROJECT_PATTERN="[A-Za-z]+"
else
# Make pattern case-insensitive by adding both cases if it's a simple pattern
# For complex patterns, rely on grep -i flag
JIRA_PROJECT_PATTERN=$(echo "$JIRA_PROJECT_PATTERN" | sed 's/\[A-Z\]\+/[A-Za-z]+/g' | sed 's/\[A-Z\]/[A-Za-z]/g')
fi
JIRA_PATTERN="${JIRA_PROJECT_PATTERN}-[0-9]+"

if [ -z "$PR_TITLE" ]; then
echo "ticket=" >> $GITHUB_OUTPUT
echo "found_in=" >> $GITHUB_OUTPUT
exit 0
fi

# Case-insensitive search
if echo "$PR_TITLE" | grep -qiE "$JIRA_PATTERN"; then
TICKET=$(echo "$PR_TITLE" | grep -oiE "$JIRA_PATTERN" | head -1)
# Normalize to uppercase for Jira API (Jira ticket keys are uppercase)
TICKET=$(echo "$TICKET" | tr '[:lower:]' '[:upper:]')
echo "ticket=$TICKET" >> $GITHUB_OUTPUT
echo "found_in=pr_title" >> $GITHUB_OUTPUT
echo "✅ Found Jira ticket in PR title: $TICKET"
exit 0
fi

echo "ticket=" >> $GITHUB_OUTPUT
echo "found_in=" >> $GITHUB_OUTPUT

- name: Extract Jira ticket from PR description
id: pr-description-check
if: steps.pr-title-check.outputs.ticket == ''
run: |
PR_BODY="${{ github.event.pull_request.body }}"
JIRA_PROJECT_PATTERN="${{ inputs.jira-project-pattern }}"
if [ -z "$JIRA_PROJECT_PATTERN" ]; then
JIRA_PROJECT_PATTERN="[A-Za-z]+"
else
# Make pattern case-insensitive by adding both cases if it's a simple pattern
# For complex patterns, rely on grep -i flag
JIRA_PROJECT_PATTERN=$(echo "$JIRA_PROJECT_PATTERN" | sed 's/\[A-Z\]\+/[A-Za-z]+/g' | sed 's/\[A-Z\]/[A-Za-z]/g')
fi
JIRA_PATTERN="${JIRA_PROJECT_PATTERN}-[0-9]+"

if [ -z "$PR_BODY" ]; then
echo "ticket=" >> $GITHUB_OUTPUT
echo "found_in=" >> $GITHUB_OUTPUT
exit 0
fi

# Case-insensitive search
if echo "$PR_BODY" | grep -qiE "$JIRA_PATTERN"; then
TICKET=$(echo "$PR_BODY" | grep -oiE "$JIRA_PATTERN" | head -1)
# Normalize to uppercase for Jira API (Jira ticket keys are uppercase)
TICKET=$(echo "$TICKET" | tr '[:lower:]' '[:upper:]')
echo "ticket=$TICKET" >> $GITHUB_OUTPUT
echo "found_in=pr_description" >> $GITHUB_OUTPUT
echo "✅ Found Jira ticket in PR description: $TICKET"
exit 0
fi

echo "ticket=" >> $GITHUB_OUTPUT
echo "found_in=" >> $GITHUB_OUTPUT

- name: Extract Jira ticket from commits
id: commit-check
if: steps.pr-description-check.outputs.ticket == ''
run: |
CHECK_COMMITS="${{ inputs.check-commits }}"
if [ "$CHECK_COMMITS" != "true" ]; then
echo "ticket=" >> $GITHUB_OUTPUT
echo "found_in=" >> $GITHUB_OUTPUT
exit 0
fi

JIRA_PROJECT_PATTERN="${{ inputs.jira-project-pattern }}"
if [ -z "$JIRA_PROJECT_PATTERN" ]; then
JIRA_PROJECT_PATTERN="[A-Z]+"
fi
JIRA_PATTERN="${JIRA_PROJECT_PATTERN}-[0-9]+"
BASE_SHA="${{ github.event.pull_request.base.sha }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"

if [ -z "$BASE_SHA" ] || [ -z "$HEAD_SHA" ]; then
echo "ticket=" >> $GITHUB_OUTPUT
echo "found_in=" >> $GITHUB_OUTPUT
exit 0
fi

# Get all commit messages between base and head
COMMIT_MESSAGES=$(git log --pretty=format:"%s" ${BASE_SHA}..${HEAD_SHA} 2>/dev/null || echo "")

# Case-insensitive search
if [ -n "$COMMIT_MESSAGES" ] && echo "$COMMIT_MESSAGES" | grep -qiE "$JIRA_PATTERN"; then
TICKET=$(echo "$COMMIT_MESSAGES" | grep -oiE "$JIRA_PATTERN" | head -1)
# Normalize to uppercase for Jira API (Jira ticket keys are uppercase)
TICKET=$(echo "$TICKET" | tr '[:lower:]' '[:upper:]')
echo "ticket=$TICKET" >> $GITHUB_OUTPUT
echo "found_in=commits" >> $GITHUB_OUTPUT
echo "✅ Found Jira ticket in commit messages: $TICKET"
exit 0
fi

echo "ticket=" >> $GITHUB_OUTPUT
echo "found_in=" >> $GITHUB_OUTPUT

- name: Determine final ticket
id: final-ticket
run: |
if [ -n "${{ steps.branch-check.outputs.ticket }}" ]; then
echo "ticket=${{ steps.branch-check.outputs.ticket }}" >> $GITHUB_OUTPUT
echo "found_in=${{ steps.branch-check.outputs.found_in }}" >> $GITHUB_OUTPUT
elif [ -n "${{ steps.pr-title-check.outputs.ticket }}" ]; then
echo "ticket=${{ steps.pr-title-check.outputs.ticket }}" >> $GITHUB_OUTPUT
echo "found_in=${{ steps.pr-title-check.outputs.found_in }}" >> $GITHUB_OUTPUT
elif [ -n "${{ steps.pr-description-check.outputs.ticket }}" ]; then
echo "ticket=${{ steps.pr-description-check.outputs.ticket }}" >> $GITHUB_OUTPUT
echo "found_in=${{ steps.pr-description-check.outputs.found_in }}" >> $GITHUB_OUTPUT
elif [ -n "${{ steps.commit-check.outputs.ticket }}" ]; then
echo "ticket=${{ steps.commit-check.outputs.ticket }}" >> $GITHUB_OUTPUT
echo "found_in=${{ steps.commit-check.outputs.found_in }}" >> $GITHUB_OUTPUT
else
echo "ticket=" >> $GITHUB_OUTPUT
echo "found_in=" >> $GITHUB_OUTPUT
fi

- name: Fail if no Jira ticket found
if: steps.final-ticket.outputs.ticket == ''
run: |
FAIL_ON_MISSING="${{ inputs.fail-on-missing }}"
if [ "$FAIL_ON_MISSING" != "true" ] && [ -n "$FAIL_ON_MISSING" ]; then
echo "⚠️ No Jira ticket found, but fail-on-missing is disabled"
exit 0
fi

# Default to true if not specified (for direct PR triggers)
if [ -z "$FAIL_ON_MISSING" ]; then
FAIL_ON_MISSING="true"
fi

if [ "$FAIL_ON_MISSING" != "true" ]; then
exit 0
fi

echo "❌ No Jira ticket found in branch name, PR title, PR description"
CHECK_COMMITS="${{ inputs.check-commits }}"
if [ "$CHECK_COMMITS" == "true" ]; then
echo " or commit messages."
else
echo "."
fi
echo ""
echo "Please ensure one of the following contains a Jira ticket key (e.g., DEVEX-600, DEV-123):"
echo " - Branch name: ${{ github.head_ref }}"
echo " - PR title: ${{ github.event.pull_request.title }}"
echo " - PR description"
if [ "$CHECK_COMMITS" == "true" ]; then
echo " - Commit messages"
fi
echo ""
echo "Jira ticket format: [PROJECT-KEY]-[NUMBER] (e.g., DEVEX-600)"
exit 1

- name: Verify ticket exists in Jira
id: verify-ticket
if: steps.final-ticket.outputs.ticket != ''
run: |
VERIFY_TICKET="${{ inputs.verify-ticket-exists }}"
# Default to true if not specified (for direct PR triggers or when default is used)
if [ -z "$VERIFY_TICKET" ]; then
VERIFY_TICKET="true"
fi

if [ "$VERIFY_TICKET" = "false" ]; then
echo "⚠️ Ticket verification is disabled, skipping API check"
echo "exists=true" >> $GITHUB_OUTPUT
exit 0
fi

JIRA_EMAIL="${{ secrets.JIRA_GITHUB_USER_EMAIL }}"
JIRA_TOKEN="${{ secrets.JIRA_GITHUB_USER_API_TOKEN }}"

if [ -z "$JIRA_EMAIL" ] || [ -z "$JIRA_TOKEN" ]; then
echo "⚠️ Jira credentials not provided, skipping ticket verification"
echo "⚠️ To enable verification, provide jira-email and jira-api-token secrets"
echo "exists=true" >> $GITHUB_OUTPUT
exit 0
fi

TICKET="${{ steps.final-ticket.outputs.ticket }}"
JIRA_INSTANCE="${{ inputs.jira-instance }}"
if [ -z "$JIRA_INSTANCE" ]; then
JIRA_INSTANCE="revolutionparts.atlassian.net"
fi

# Create base64 encoded credentials
AUTH_STRING=$(echo -n "${JIRA_EMAIL}:${JIRA_TOKEN}" | base64)

echo "🔍 Verifying ticket $TICKET exists in Jira..."

# Make API request to check if ticket exists
# Use a unique delimiter to separate body from HTTP code
TEMP_FILE=$(mktemp)
HTTP_CODE=$(curl -s \
-X GET \
-H "Authorization: Basic ${AUTH_STRING}" \
-H "Accept: application/json" \
-w "%{http_code}" \
-o "$TEMP_FILE" \
"https://${JIRA_INSTANCE}/rest/api/3/issue/${TICKET}")

# Read the body from the temp file (this is clean JSON)
BODY=$(cat "$TEMP_FILE")
rm -f "$TEMP_FILE"

if [ "$HTTP_CODE" = "200" ]; then
echo "✅ Ticket $TICKET exists in Jira"
echo "exists=true" >> $GITHUB_OUTPUT

# Extract status from Jira API response (status is nested: fields.status.name)
# Try using jq first (if available), then fall back to grep/sed
if command -v jq >/dev/null 2>&1; then
STATUS=$(echo "$BODY" | jq -r '.fields.status.name // empty' 2>/dev/null || echo "")
fi

if [ -z "$STATUS" ]; then
# Fallback: extract status using grep/sed pattern matching
# Look for "status" object followed by "name" field
STATUS=$(echo "$BODY" | grep -o '"status"[^}]*"name":"[^"]*' | sed 's/.*"name":"\([^"]*\)".*/\1/' | head -1 || echo "")
fi

if [ -z "$STATUS" ]; then
# Alternative: look for any "name" field that appears after "status" in the JSON
STATUS=$(echo "$BODY" | sed -n 's/.*"status"[^}]*"name":"\([^"]*\)".*/\1/p' | head -1 || echo "")
fi

# Normalize status to lowercase for comparison
STATUS_LOWER=$(echo "$STATUS" | tr '[:upper:]' '[:lower:]')

# Check if status is in the list of invalid statuses
INVALID_STATUSES="backlog|open|closed|done|ready to release"
if echo "$STATUS_LOWER" | grep -qiE "($INVALID_STATUSES)"; then
echo "❌ Ticket $TICKET is in an invalid status: $STATUS"
echo ""
echo "Tickets in the following statuses cannot have work logged against them:"
echo " - Backlog"
echo " - Open"
echo " - Closed/Done"
echo " - Ready to Release"
echo ""
echo "Please move the ticket to an active status before creating a PR."
exit 1
fi

if [ -n "$TITLE" ]; then
echo " Title: $TITLE"
fi
if [ -n "$STATUS" ]; then
echo " Status: $STATUS"
fi
elif [ "$HTTP_CODE" = "404" ]; then
echo "❌ Ticket $TICKET does not exist in Jira"
echo "exists=false" >> $GITHUB_OUTPUT
exit 1
elif [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "403" ]; then
echo "⚠️ Authentication failed or insufficient permissions to verify ticket"
echo "⚠️ HTTP Status: $HTTP_CODE"
echo "⚠️ Continuing without verification..."
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "⚠️ Unexpected response from Jira API"
echo "⚠️ HTTP Status: $HTTP_CODE"
echo "⚠️ Response: $BODY"
echo "⚠️ Continuing without verification..."
echo "exists=true" >> $GITHUB_OUTPUT
fi

- name: Fail if ticket does not exist
if: steps.final-ticket.outputs.ticket != '' && steps.verify-ticket.outputs.exists == 'false'
run: |
TICKET="${{ steps.final-ticket.outputs.ticket }}"
JIRA_INSTANCE="${{ inputs.jira-instance }}"
if [ -z "$JIRA_INSTANCE" ]; then
JIRA_INSTANCE="revolutionparts.atlassian.net"
fi
echo "❌ Jira ticket $TICKET does not exist in Jira"
echo ""
echo "The ticket key was found in your PR, but it does not exist in Jira."
echo "Please ensure you're using a valid Jira ticket key."
echo ""
echo "Link: https://${JIRA_INSTANCE}/browse/$TICKET"
exit 1

- name: Success message
if: steps.final-ticket.outputs.ticket != '' && (steps.verify-ticket.outputs.exists == 'true' || steps.verify-ticket.outputs.exists == '')
run: |
TICKET="${{ steps.final-ticket.outputs.ticket }}"
JIRA_INSTANCE="${{ inputs.jira-instance }}"
if [ -z "$JIRA_INSTANCE" ]; then
JIRA_INSTANCE="revolutionparts.atlassian.net"
fi
echo "✅ Jira ticket found: $TICKET"
echo " Found in: ${{ steps.final-ticket.outputs.found_in }}"
echo " Link: https://${JIRA_INSTANCE}/browse/$TICKET"