#!/usr/bin/env bash set -euo pipefail # Initialize a comma-separated string to hold PR numbers that need a comment PRS_NEEDING_COMMENT="" # Function to process a single PR process_pr() { if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then echo "‼️ Missing \$GITHUB_REPOSITORY - this must be run from GitHub Actions" return 1 fi if [[ -z "${GITHUB_OUTPUT:-}" ]]; then echo "‼️ Missing \$GITHUB_OUTPUT - this must be run from GitHub Actions" return 1 fi local PR_NUMBER=$1 echo "🔄 Processing PR #${PR_NUMBER}" # Get PR body with error handling local PR_BODY if ! PR_BODY=$(gh pr view "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --json body -q .body 2>/dev/null); then echo " ⚠️ Could not fetch PR #${PR_NUMBER} details" return 1 fi # Look for issue references using multiple patterns local ISSUE_NUMBER="" # Pattern 1: Direct reference like #123 if [[ -z "${ISSUE_NUMBER}" ]]; then ISSUE_NUMBER=$(echo "${PR_BODY}" | grep -oE '#[0-9]+' | head -1 | sed 's/#//' 2>/dev/null || echo "") fi # Pattern 2: Closes/Fixes/Resolves patterns (case-insensitive) if [[ -z "${ISSUE_NUMBER}" ]]; then ISSUE_NUMBER=$(echo "${PR_BODY}" | grep -iE '(closes?|fixes?|resolves?) #[0-9]+' | grep -oE '#[0-9]+' | head -1 | sed 's/#//' 2>/dev/null || echo "") fi if [[ -z "${ISSUE_NUMBER}" ]]; then echo "⚠️ No linked issue found for PR #${PR_NUMBER}, adding status/need-issue label" if ! gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --add-label "status/need-issue" 2>/dev/null; then echo " ⚠️ Failed to add label (may already exist or have permission issues)" fi # Add PR number to the list if [[ -z "${PRS_NEEDING_COMMENT}" ]]; then PRS_NEEDING_COMMENT="${PR_NUMBER}" else PRS_NEEDING_COMMENT="${PRS_NEEDING_COMMENT},${PR_NUMBER}" fi echo "needs_comment=true" >> "${GITHUB_OUTPUT}" else echo "🔗 Found linked issue #${ISSUE_NUMBER}" # Remove status/need-issue label if present if ! gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --remove-label "status/need-issue" 2>/dev/null; then echo " status/need-issue label not present or could not be removed" fi # Get issue labels echo "📥 Fetching labels from issue #${ISSUE_NUMBER}" local ISSUE_LABELS="" if ! ISSUE_LABELS=$(gh issue view "${ISSUE_NUMBER}" --repo "${GITHUB_REPOSITORY}" --json labels -q '.labels[].name' 2>/dev/null | tr '\n' ',' | sed 's/,$//' || echo ""); then echo " ⚠️ Could not fetch issue #${ISSUE_NUMBER} (may not exist or be in different repo)" ISSUE_LABELS="" fi # Get PR labels echo "📥 Fetching labels from PR #${PR_NUMBER}" local PR_LABELS="" if ! PR_LABELS=$(gh pr view "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --json labels -q '.labels[].name' 2>/dev/null | tr '\n' ',' | sed 's/,$//' || echo ""); then echo " ⚠️ Could not fetch PR labels" PR_LABELS="" fi echo " Issue labels: ${ISSUE_LABELS}" echo " PR labels: ${PR_LABELS}" # Convert comma-separated strings to arrays local ISSUE_LABEL_ARRAY PR_LABEL_ARRAY IFS=',' read -ra ISSUE_LABEL_ARRAY <<< "${ISSUE_LABELS}" IFS=',' read -ra PR_LABEL_ARRAY <<< "${PR_LABELS}" # Find labels to add (on issue but not on PR) local LABELS_TO_ADD="" for label in "${ISSUE_LABEL_ARRAY[@]}"; do if [[ -n "${label}" ]] && [[ " ${PR_LABEL_ARRAY[*]} " != *" ${label} "* ]]; then if [[ -z "${LABELS_TO_ADD}" ]]; then LABELS_TO_ADD="${label}" else LABELS_TO_ADD="${LABELS_TO_ADD},${label}" fi fi done # Find labels to remove (on PR but not on issue) local LABELS_TO_REMOVE="" for label in "${PR_LABEL_ARRAY[@]}"; do if [[ -n "${label}" ]] && [[ " ${ISSUE_LABEL_ARRAY[*]} " != *" ${label} "* ]]; then # Don't remove status/need-issue since we already handled it if [[ "${label}" != "status/need-issue" ]]; then if [[ -z "${LABELS_TO_REMOVE}" ]]; then LABELS_TO_REMOVE="${label}" else LABELS_TO_REMOVE="${LABELS_TO_REMOVE},${label}" fi fi fi done # Apply label changes if [[ -n "${LABELS_TO_ADD}" ]]; then echo "➕ Adding labels: ${LABELS_TO_ADD}" if ! gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --add-label "${LABELS_TO_ADD}" 2>/dev/null; then echo " ⚠️ Failed to add some labels" fi fi if [[ -n "${LABELS_TO_REMOVE}" ]]; then echo "➖ Removing labels: ${LABELS_TO_REMOVE}" if ! gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --remove-label "${LABELS_TO_REMOVE}" 2>/dev/null; then echo " ⚠️ Failed to remove some labels" fi fi if [[ -z "${LABELS_TO_ADD}" ]] && [[ -z "${LABELS_TO_REMOVE}" ]]; then echo "✅ Labels already synchronized" fi echo "needs_comment=false" >> "${GITHUB_OUTPUT}" fi } # If PR_NUMBER is set, process only that PR if [[ -n "${PR_NUMBER:-}" ]]; then if ! process_pr "${PR_NUMBER}"; then echo "❌ Failed to process PR #${PR_NUMBER}" exit 1 fi else # Otherwise, get all open PRs and process them # The script logic will determine which ones need issue linking or label sync echo "📥 Getting all open pull requests..." if ! PR_NUMBERS=$(gh pr list --repo "${GITHUB_REPOSITORY}" --state open --limit 1000 --json number -q '.[].number' 2>/dev/null); then echo "❌ Failed to fetch PR list" exit 1 fi if [[ -z "${PR_NUMBERS}" ]]; then echo "✅ No open PRs found" else # Count the number of PRs PR_COUNT=$(echo "${PR_NUMBERS}" | wc -w | tr -d ' ') echo "📊 Found ${PR_COUNT} open PRs to process" for pr_number in ${PR_NUMBERS}; do if ! process_pr "${pr_number}"; then echo "⚠️ Failed to process PR #${pr_number}, continuing with next PR..." continue fi done fi fi # Ensure output is always set, even if empty if [[ -z "${PRS_NEEDING_COMMENT}" ]]; then echo "prs_needing_comment=[]" >> "${GITHUB_OUTPUT}" else echo "prs_needing_comment=[${PRS_NEEDING_COMMENT}]" >> "${GITHUB_OUTPUT}" fi echo "✅ PR triage completed"