diff --git a/.github/actions/add-to-project/action.yml b/.github/actions/add-to-project/action.yml index d528ab1..3fe9a9e 100644 --- a/.github/actions/add-to-project/action.yml +++ b/.github/actions/add-to-project/action.yml @@ -84,18 +84,28 @@ runs: echo "Contents of parent_child_links.json before adding issues to the project:" cat parent_child_links.json - # Add all issues to the project (using target repository issues) - for source_parent in $(jq -r 'keys_unsorted[]' parent_child_links.json); do + # Initialize file to track project item IDs + echo "{}" > project_item_mapping.json + + # Get all unique issue numbers that need to be added to the project + ALL_PARENT_ISSUES=$(jq -r 'keys_unsorted[]' parent_child_links.json) + ALL_CHILD_ISSUES=$(jq -r '.[] | .[].number' parent_child_links.json) + ALL_UNIQUE_ISSUES=$(echo -e "$ALL_PARENT_ISSUES\n$ALL_CHILD_ISSUES" | sort -n | uniq) + + echo "All issues to add to project: $ALL_UNIQUE_ISSUES" + + # Add all issues to the project first (both parents and children) + for source_issue_number in $ALL_UNIQUE_ISSUES; do # Get the target issue number from mapping - TARGET_PARENT=$(jq -r --arg source "$source_parent" '.[$source] // empty' source_target_mapping.json) + TARGET_ISSUE=$(jq -r --arg source "$source_issue_number" '.[$source] // empty' source_target_mapping.json) - if [ -z "$TARGET_PARENT" ] || [ "$TARGET_PARENT" = "null" ]; then - echo "Warning: No target mapping found for source issue #$source_parent" - log_broken_url "https://github.com/$SOURCE_OWNER/$SOURCE_REPO/issues/$source_parent" "No target mapping found" + if [ -z "$TARGET_ISSUE" ] || [ "$TARGET_ISSUE" = "null" ]; then + echo "Warning: No target mapping found for source issue #$source_issue_number" + log_broken_url "https://github.com/$SOURCE_OWNER/$SOURCE_REPO/issues/$source_issue_number" "No target mapping found" continue fi - echo "Adding target issue #$TARGET_PARENT (from source #$source_parent) to the project" + echo "Adding target issue #$TARGET_ISSUE (from source #$source_issue_number) to the project" # Check rate limit before GraphQL query for issue ID check_rate_limit @@ -109,13 +119,13 @@ runs: } }' - ISSUE_RESPONSE=$(gh api graphql -f query="$ISSUE_QUERY" -f owner="$TARGET_OWNER" -f repo="$TARGET_REPO" -F issueNumber="$TARGET_PARENT") + ISSUE_RESPONSE=$(gh api graphql -f query="$ISSUE_QUERY" -f owner="$TARGET_OWNER" -f repo="$TARGET_REPO" -F issueNumber="$TARGET_ISSUE") ISSUE_ID=$(echo "$ISSUE_RESPONSE" | jq -r '.data.repository.issue.id') echo "Target Issue Global ID: ${ISSUE_ID}" if [ -z "$ISSUE_ID" ] || [ "$ISSUE_ID" = "null" ]; then - echo "Warning: Could not get Global ID for target issue #$TARGET_PARENT" - log_broken_url "https://github.com/$TARGET_OWNER/$TARGET_REPO/issues/$TARGET_PARENT" "Could not get Global ID" + echo "Warning: Could not get Global ID for target issue #$TARGET_ISSUE" + log_broken_url "https://github.com/$TARGET_OWNER/$TARGET_REPO/issues/$TARGET_ISSUE" "Could not get Global ID" continue fi @@ -134,14 +144,36 @@ runs: ADD_RESPONSE=$(gh api graphql -f query="$ADD_MUTATION" -f projectId="$PROJECT_ID" -f contentId="$ISSUE_ID") echo "Add response: $ADD_RESPONSE" - echo "Target issue #$TARGET_PARENT added to the project" + # Extract project item ID and store mapping + PROJECT_ITEM_ID=$(echo "$ADD_RESPONSE" | jq -r '.data.addProjectV2ItemById.item.id // empty') + if [ -n "$PROJECT_ITEM_ID" ] && [ "$PROJECT_ITEM_ID" != "null" ]; then + # Map target issue number to project item ID + jq --arg issue "$TARGET_ISSUE" \ + --arg item_id "$PROJECT_ITEM_ID" \ + '.[$issue] = $item_id' \ + project_item_mapping.json > project_item_mapping_temp.json && \ + mv project_item_mapping_temp.json project_item_mapping.json + echo "Target issue #$TARGET_ISSUE added to project with item ID: $PROJECT_ITEM_ID" + else + echo "Warning: Failed to get project item ID for issue #$TARGET_ISSUE" + fi done - # Log the contents of the JSON file before linking child issues + # Log the contents of the JSON files before linking child issues echo "Contents of parent_child_links.json before linking child issues:" cat parent_child_links.json + echo "Contents of project_item_mapping.json:" + cat project_item_mapping.json + + # Note: Using addSubIssue mutation instead of project field updates + # This establishes parent-child relationships at the repository level, + # which should automatically reflect in any GitHub Projects that include these issues - # Link child issues to their parents (using target repository issue numbers) + # Now establish parent-child relationships using the addSubIssue mutation + # Based on GitHub's documentation: https://github.com/orgs/community/discussions/148714 + # The correct approach is to use the addSubIssue mutation instead of updating project field values + echo "Establishing parent-child relationships using addSubIssue mutation..." + for source_parent in $(jq -r 'keys_unsorted[]' parent_child_links.json); do # Get the target parent issue number TARGET_PARENT=$(jq -r --arg source "$source_parent" '.[$source] // empty' source_target_mapping.json) @@ -151,6 +183,30 @@ runs: continue fi + # Get the parent issue's Global ID from the target repository + echo "Getting parent issue Global ID for issue #$TARGET_PARENT..." + + # Check rate limit before GraphQL query + check_rate_limit + + PARENT_ISSUE_QUERY='query($owner: String!, $repo: String!, $issueNumber: Int!) { + repository(owner: $owner, name: $repo) { + issue(number: $issueNumber) { + id + } + } + }' + + PARENT_ISSUE_RESPONSE=$(gh api graphql -f query="$PARENT_ISSUE_QUERY" -f owner="$TARGET_OWNER" -f repo="$TARGET_REPO" -F issueNumber="$TARGET_PARENT") + PARENT_ISSUE_GLOBAL_ID=$(echo "$PARENT_ISSUE_RESPONSE" | jq -r '.data.repository.issue.id') + + if [ -z "$PARENT_ISSUE_GLOBAL_ID" ] || [ "$PARENT_ISSUE_GLOBAL_ID" = "null" ]; then + echo "Warning: Could not get Global ID for parent issue #$TARGET_PARENT" + continue + fi + + echo "Parent issue #$TARGET_PARENT Global ID: $PARENT_ISSUE_GLOBAL_ID" + for child in $(jq -c --arg parent "$source_parent" '.[$parent][]' parent_child_links.json); do SOURCE_CHILD_NUMBER=$(echo "$child" | jq -r '.number') @@ -163,36 +219,78 @@ runs: continue fi - echo "Linking target child issue #$TARGET_CHILD to target parent issue #$TARGET_PARENT (from source #$SOURCE_CHILD_NUMBER -> #$source_parent)" + # Get the child issue's Global ID from the target repository + echo "Getting child issue Global ID for issue #$TARGET_CHILD..." - # Check if child issue exists in target repository before linking - # Check rate limit before REST API call + # Check rate limit before GraphQL query check_rate_limit - CHILD_CHECK_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "Authorization: token $GITHUB_TOKEN" \ - "https://api.github.com/repos/$TARGET_OWNER/$TARGET_REPO/issues/$TARGET_CHILD") + CHILD_ISSUE_QUERY='query($owner: String!, $repo: String!, $issueNumber: Int!) { + repository(owner: $owner, name: $repo) { + issue(number: $issueNumber) { + id + } + } + }' - if [ "$CHILD_CHECK_RESPONSE" = "200" ]; then - # Check rate limit before REST API call - check_rate_limit - - COMMENT_RESPONSE=$(curl -s -X POST \ - -H "Authorization: token $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github.v3+json" \ - -d "{\"body\": \"Child of: #$TARGET_PARENT\"}" \ - "https://api.github.com/repos/$TARGET_OWNER/$TARGET_REPO/issues/$TARGET_CHILD/comments") + CHILD_ISSUE_RESPONSE=$(gh api graphql -f query="$CHILD_ISSUE_QUERY" -f owner="$TARGET_OWNER" -f repo="$TARGET_REPO" -F issueNumber="$TARGET_CHILD") + CHILD_ISSUE_GLOBAL_ID=$(echo "$CHILD_ISSUE_RESPONSE" | jq -r '.data.repository.issue.id') + + if [ -z "$CHILD_ISSUE_GLOBAL_ID" ] || [ "$CHILD_ISSUE_GLOBAL_ID" = "null" ]; then + echo "Warning: Could not get Global ID for child issue #$TARGET_CHILD" + continue + fi + + echo "Child issue #$TARGET_CHILD Global ID: $CHILD_ISSUE_GLOBAL_ID" + + # Establish parent-child relationship using addSubIssue mutation + echo "Setting parent-child relationship: Parent issue #$TARGET_PARENT -> Child issue #$TARGET_CHILD" + + # Check rate limit before GraphQL mutation + check_rate_limit + + ADD_SUB_ISSUE_MUTATION='mutation($issueId: ID!, $subIssueId: ID!) { + addSubIssue(input: { + issueId: $issueId, + subIssueId: $subIssueId + }) { + issue { + id + number + title + } + subIssue { + id + number + title + } + } + }' + + ADD_SUB_ISSUE_RESPONSE=$(gh api graphql \ + -f query="$ADD_SUB_ISSUE_MUTATION" \ + -f issueId="$PARENT_ISSUE_GLOBAL_ID" \ + -f subIssueId="$CHILD_ISSUE_GLOBAL_ID" 2>&1 || true) + + echo "addSubIssue response: $ADD_SUB_ISSUE_RESPONSE" + + # Check if the mutation succeeded + if echo "$ADD_SUB_ISSUE_RESPONSE" | jq -e '.data.addSubIssue.issue.id' >/dev/null 2>&1; then + echo "SUCCESS: Parent-child relationship established: Parent issue #$TARGET_PARENT -> Child issue #$TARGET_CHILD" + else + echo "ERROR: Failed to establish parent-child relationship" - # Check if the comment was created successfully - if echo "$COMMENT_RESPONSE" | jq -e '.id' >/dev/null 2>&1; then - echo "Child Issue #$TARGET_CHILD linked to Parent Issue #$TARGET_PARENT" + # Extract and log specific error messages + if echo "$ADD_SUB_ISSUE_RESPONSE" | jq -e '.errors' >/dev/null 2>&1; then + echo "GraphQL errors:" + echo "$ADD_SUB_ISSUE_RESPONSE" | jq -r '.errors[] | "- \(.message)"' + echo "$ADD_SUB_ISSUE_RESPONSE" | jq -r '.errors[].extensions // empty' else - echo "Warning: Failed to link child issue #$TARGET_CHILD to parent #$TARGET_PARENT" - log_broken_url "https://github.com/$TARGET_OWNER/$TARGET_REPO/issues/$TARGET_CHILD" "Failed to create parent link comment" + echo "No JSON errors found. Raw response:" + echo "$ADD_SUB_ISSUE_RESPONSE" fi - else - echo "Warning: Child issue #$TARGET_CHILD does not exist in target repository" - log_broken_url "https://github.com/$TARGET_OWNER/$TARGET_REPO/issues/$TARGET_CHILD" "Child issue does not exist in target repository (HTTP $CHILD_CHECK_RESPONSE)" + + log_broken_url "https://github.com/$TARGET_OWNER/$TARGET_REPO/issues/$TARGET_CHILD" "Failed to establish parent-child relationship using addSubIssue mutation: $(echo "$ADD_SUB_ISSUE_RESPONSE" | jq -r '.errors[0].message // "Unknown error"')" fi done done \ No newline at end of file diff --git a/.github/actions/copy-issues/action.yml b/.github/actions/copy-issues/action.yml index c010d38..547a627 100644 --- a/.github/actions/copy-issues/action.yml +++ b/.github/actions/copy-issues/action.yml @@ -41,7 +41,15 @@ runs: # Combine parent and child issues, remove duplicates ALL_UNIQUE_ISSUES=$(echo -e "$ALL_ISSUES\n$CHILD_ISSUES" | sort -n | uniq) - echo "Issues to copy: $ALL_UNIQUE_ISSUES" + echo "=== COPY ANALYSIS ===" + echo "Parent issues from source: $ALL_ISSUES" + echo "Child issues from source: $CHILD_ISSUES" + echo "All unique issues to copy: $ALL_UNIQUE_ISSUES" + TOTAL_TO_COPY=$(echo "$ALL_UNIQUE_ISSUES" | wc -w) + echo "Total issues to copy: $TOTAL_TO_COPY" + + SUCCESSFULLY_COPIED=0 + FAILED_TO_COPY=0 # Copy each issue from source to target repository for source_issue_number in $ALL_UNIQUE_ISSUES; do @@ -57,7 +65,9 @@ runs: # Check if source issue exists if ! echo "$SOURCE_ISSUE_RESPONSE" | jq -e '.number' >/dev/null 2>&1; then echo "Warning: Source issue #$source_issue_number not found or invalid response" + echo "Response: $SOURCE_ISSUE_RESPONSE" log_broken_url "https://github.com/$SOURCE_OWNER/$SOURCE_REPO/issues/$source_issue_number" "Source issue not found" + FAILED_TO_COPY=$((FAILED_TO_COPY + 1)) continue fi @@ -103,13 +113,27 @@ runs: mv source_target_mapping_temp.json source_target_mapping.json echo "Mapped source issue #$source_issue_number -> target issue #$TARGET_ISSUE_NUMBER" + SUCCESSFULLY_COPIED=$((SUCCESSFULLY_COPIED + 1)) else echo "Error: Failed to create issue in target repository" echo "Response: $CREATE_RESPONSE" log_broken_url "https://github.com/$TARGET_OWNER/$TARGET_REPO" "Failed to create issue from source #$source_issue_number" + FAILED_TO_COPY=$((FAILED_TO_COPY + 1)) fi done + echo "=== COPY SUMMARY ===" + echo "Total issues to copy: $TOTAL_TO_COPY" + echo "Successfully copied: $SUCCESSFULLY_COPIED" + echo "Failed to copy: $FAILED_TO_COPY" + + if [ "$SUCCESSFULLY_COPIED" -eq "$TOTAL_TO_COPY" ]; then + echo "SUCCESS: All source issues successfully copied to target repository" + else + echo "WARNING: Issue copying incomplete!" + echo "This will cause relationship preservation failures in subsequent steps." + fi + echo "=== Issue Copying Complete ===" echo "Source-to-target mapping:" cat source_target_mapping.json \ No newline at end of file diff --git a/.github/actions/fetch-hierarchy/action.yml b/.github/actions/fetch-hierarchy/action.yml index 190a502..c946142 100644 --- a/.github/actions/fetch-hierarchy/action.yml +++ b/.github/actions/fetch-hierarchy/action.yml @@ -131,10 +131,13 @@ runs: echo "subIssues query failed or returned errors, trying alternative approach" fi - # If subIssues didn't work, try timeline items and body parsing + # If subIssues didn't work, parse task list from body to find sub-issues if [ -z "$SUBISSUES" ]; then - QUERY2="query(\$issue_id: ID!) { node(id: \$issue_id) { ... on Issue { id number body timelineItems(first: 100, itemTypes: [CROSS_REFERENCED_EVENT]) { nodes { ... on CrossReferencedEvent { source { ... on Issue { id number title url } } } } } } } }" - echo "Trying timeline items query: $QUERY2" + echo "Native subIssues field not available, trying task list parsing from issue body..." + + # Fetch issue body only + QUERY2="query(\$issue_id: ID!) { node(id: \$issue_id) { ... on Issue { id number body } } }" + echo "Fetching issue body: $QUERY2" # Check rate limit before GraphQL query check_rate_limit @@ -147,42 +150,52 @@ runs: jq '.errors' subissues_response.json return 1 fi - - echo "Raw response for alternative query:" - cat subissues_response.json - # Try cross-referenced issues from timeline - if jq -e '.data.node.timelineItems.nodes' subissues_response.json > /dev/null 2>&1; then - TIMELINE_ISSUES=$(jq -c '.data.node.timelineItems.nodes[] | select(.source.number != null) | .source' subissues_response.json) - if [ -n "$TIMELINE_ISSUES" ]; then - echo "Found cross-referenced issues for issue #$issue_number:" - echo "$TIMELINE_ISSUES" | jq -r '.number' - SUBISSUES="$TIMELINE_ISSUES" + # Parse task list from body to find genuine sub-issues + ISSUE_BODY=$(jq -r '.data.node.body // ""' subissues_response.json) + if [[ "$ISSUE_BODY" =~ \#[0-9]+ ]]; then + echo "Found issue references in body, parsing task list for sub-issues..." + echo "Issue body excerpt:" + # Fix the broken pipe error by avoiding head command and using printf instead + if [ ${#ISSUE_BODY} -gt 500 ]; then + printf "%.500s...\n" "$ISSUE_BODY" + else + echo "$ISSUE_BODY" fi - fi - - # If still no sub-issues, parse task list from body - if [ -z "$SUBISSUES" ]; then - ISSUE_BODY=$(jq -r '.data.node.body // ""' subissues_response.json) - if [[ "$ISSUE_BODY" =~ \#[0-9]+ ]]; then - echo "Found issue references in body, parsing task list..." - # Extract issue numbers from task list items (- [ ] or - [x] #123) - TASK_ISSUE_NUMBERS=$(echo "$ISSUE_BODY" | grep -oE -- '- \[[x ]\].*#[0-9]+' | grep -oE '#[0-9]+' | sed 's/#//' | sort -u) - if [ -n "$TASK_ISSUE_NUMBERS" ]; then - echo "Found task list sub-issues: $TASK_ISSUE_NUMBERS" - # Convert to JSON format for consistency - SUBISSUES="" - for task_issue_num in $TASK_ISSUE_NUMBERS; do + + # Extract issue numbers ONLY from task list items (- [ ] or - [x] #123) + # This is more restrictive than timeline approach - only actual checklist items + # Use temporary file to avoid broken pipe error + echo "$ISSUE_BODY" > /tmp/issue_body.txt 2>/dev/null || true + TASK_ISSUE_NUMBERS=$(grep -oE -- '^\s*- \[[x ]\].*#[0-9]+' /tmp/issue_body.txt 2>/dev/null | grep -oE '#[0-9]+' 2>/dev/null | sed 's/#//' 2>/dev/null | sort -u 2>/dev/null || true) + rm -f /tmp/issue_body.txt 2>/dev/null || true + + if [ -n "$TASK_ISSUE_NUMBERS" ]; then + echo "Found task list sub-issues (checklist items only): $TASK_ISSUE_NUMBERS" + TASK_COUNT=$(echo "$TASK_ISSUE_NUMBERS" | wc -w) + echo "Total sub-issues found in task list: $TASK_COUNT" + + # Convert to JSON format for consistency + SUBISSUES="" + for task_issue_num in $TASK_ISSUE_NUMBERS; do + # Validate the issue number is from the same repository + if [[ "$task_issue_num" =~ ^[0-9]+$ ]]; then # Create a JSON object for each task issue - TASK_ISSUE_JSON="{\"number\": $task_issue_num, \"title\": \"Task issue #$task_issue_num\", \"url\": \"https://github.com/$SOURCE_OWNER/$SOURCE_REPO/issues/$task_issue_num\"}" + TASK_ISSUE_JSON="{\"number\": $task_issue_num, \"title\": \"Sub-issue #$task_issue_num\", \"url\": \"https://github.com/$SOURCE_OWNER/$SOURCE_REPO/issues/$task_issue_num\"}" if [ -z "$SUBISSUES" ]; then SUBISSUES="$TASK_ISSUE_JSON" else SUBISSUES="$SUBISSUES\n$TASK_ISSUE_JSON" fi - done - fi + else + echo "Warning: Skipping invalid issue number: $task_issue_num" + fi + done + else + echo "No task list sub-issues found in issue body" fi + else + echo "No issue references found in body" fi fi @@ -316,4 +329,45 @@ runs: echo "Full parent-child hierarchy constructed and saved to parent_child_links.json." echo "Contents of parent_child_links.json after processing all issues:" - cat parent_child_links.json \ No newline at end of file + cat parent_child_links.json + + echo "=== HIERARCHY ANALYSIS ===" + TOTAL_TOP_LEVEL_ISSUES=$(jq '. | length' "$TOP_LEVEL_ISSUES") + PARENT_COUNT=$(jq -r 'keys | length' parent_child_links.json) + TOTAL_CHILD_COUNT=$(jq -r '[.[] | length] | add // 0' parent_child_links.json) + ALL_UNIQUE_CHILD_ISSUES=$(jq -r '[.[] | .[].number] | unique | length' parent_child_links.json) + + echo "Total top-level issues found: $TOTAL_TOP_LEVEL_ISSUES" + echo "Total parent issues found: $PARENT_COUNT" + echo "Total child relationships found: $TOTAL_CHILD_COUNT" + echo "Total unique child issues: $ALL_UNIQUE_CHILD_ISSUES" + echo "Expected total issues to copy: $((TOTAL_TOP_LEVEL_ISSUES + ALL_UNIQUE_CHILD_ISSUES - PARENT_COUNT))" + + # Validate that we're not including mentions as sub-issues + if [ "$PARENT_COUNT" -eq 0 ]; then + echo "WARNING: No parent-child relationships found!" + echo "This could indicate:" + echo "1. No issues have sub-issues in the source repository" + echo "2. The sub-issue detection method failed" + echo "3. API access issues preventing relationship discovery" + else + echo "SUCCESS: Parent-child hierarchy successfully built using task list parsing only" + echo "Hierarchy breakdown (parent -> children count):" + for parent in $(jq -r 'keys_unsorted[]' parent_child_links.json); do + child_count=$(jq -r --arg parent "$parent" '.[$parent] | length' parent_child_links.json) + child_numbers=$(jq -r --arg parent "$parent" '.[$parent] | .[].number' parent_child_links.json | tr '\n' ',' | sed 's/,$//') + echo " Parent issue #$parent -> $child_count child issue(s): [$child_numbers]" + done + + # Validate no cross-referenced mentions are included + echo "" + echo "=== SUB-ISSUE VALIDATION ===" + echo "This workflow now extracts ONLY genuine sub-issues from:" + echo "1. Native GitHub subIssues field (if available)" + echo "2. Task list items in issue body (- [ ] #123 or - [x] #123)" + echo "" + echo "EXCLUDED from extraction (avoiding mentions):" + echo "1. Timeline cross-referenced events" + echo "2. Random issue mentions in text" + echo "3. Non-checklist issue references" + fi \ No newline at end of file diff --git a/.github/actions/shared/scripts/common-functions.sh b/.github/actions/shared/scripts/common-functions.sh index 9c76b34..75c1786 100755 --- a/.github/actions/shared/scripts/common-functions.sh +++ b/.github/actions/shared/scripts/common-functions.sh @@ -92,6 +92,7 @@ log_broken_url() { initialize_json_files() { echo "{}" > parent_child_links.json echo "{}" > source_target_mapping.json + echo "{}" > project_item_mapping.json echo "[]" > broken_urls.json }