diff --git a/.github/workflows/jira-ticket-check.yaml b/.github/workflows/jira-ticket-check.yaml new file mode 100644 index 0000000..5919550 --- /dev/null +++ b/.github/workflows/jira-ticket-check.yaml @@ -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"