diff --git a/.github/workflows/pull-project-tasks.yml b/.github/workflows/pull-project-tasks.yml index df93de6..70de08e 100644 --- a/.github/workflows/pull-project-tasks.yml +++ b/.github/workflows/pull-project-tasks.yml @@ -135,17 +135,19 @@ jobs: while [ $attempt -le $max_attempts ]; do echo "Attempt $attempt of $max_attempts: $*" - # Run the command directly, not via eval - if "$@"; then - echo "Command succeeded on attempt $attempt." + # Capture both stdout and stderr, and the exit code + local response + local exit_code + response=$("$@" 2>&1) + exit_code=$? + + if [ $exit_code -eq 0 ]; then + echo "Command succeeded on attempt $attempt." >&2 + # Output only the response so calling code can capture clean JSON + echo "$response" return 0 else - local exit_code=$? echo "Command failed with exit code $exit_code on attempt $attempt." - - # Log the response from the command - local response - response=$("$@" 2>&1) echo "Command response: $response" if [ $attempt -eq $max_attempts ]; then @@ -520,17 +522,19 @@ jobs: while [ $attempt -le $max_attempts ]; do echo "Attempt $attempt of $max_attempts: $*" - # Run the command directly, not via eval - if "$@"; then - echo "Command succeeded on attempt $attempt." + # Capture both stdout and stderr, and the exit code + local response + local exit_code + response=$("$@" 2>&1) + exit_code=$? + + if [ $exit_code -eq 0 ]; then + echo "Command succeeded on attempt $attempt." >&2 + # Output only the response so calling code can capture clean JSON + echo "$response" return 0 else - local exit_code=$? echo "Command failed with exit code $exit_code on attempt $attempt." - - # Log the response from the command - local response - response=$("$@" 2>&1) echo "Command response: $response" if [ $attempt -eq $max_attempts ]; then @@ -731,17 +735,19 @@ jobs: while [ $attempt -le $max_attempts ]; do echo "Attempt $attempt of $max_attempts: $*" - # Run the command directly, not via eval - if "$@"; then - echo "Command succeeded on attempt $attempt." + # Capture both stdout and stderr, and the exit code + local response + local exit_code + response=$("$@" 2>&1) + exit_code=$? + + if [ $exit_code -eq 0 ]; then + echo "Command succeeded on attempt $attempt." >&2 + # Output only the response so calling code can capture clean JSON + echo "$response" return 0 else - local exit_code=$? echo "Command failed with exit code $exit_code on attempt $attempt." - - # Log the response from the command - local response - response=$("$@" 2>&1) echo "Command response: $response" if [ $attempt -eq $max_attempts ]; then @@ -955,17 +961,19 @@ jobs: while [ $attempt -le $max_attempts ]; do echo "Attempt $attempt of $max_attempts: $*" - # Run the command directly, not via eval - if "$@"; then - echo "Command succeeded on attempt $attempt." + # Capture both stdout and stderr, and the exit code + local response + local exit_code + response=$("$@" 2>&1) + exit_code=$? + + if [ $exit_code -eq 0 ]; then + echo "Command succeeded on attempt $attempt." >&2 + # Output only the response so calling code can capture clean JSON + echo "$response" return 0 else - local exit_code=$? echo "Command failed with exit code $exit_code on attempt $attempt." - - # Log the response from the command - local response - response=$("$@" 2>&1) echo "Command response: $response" if [ $attempt -eq $max_attempts ]; then diff --git a/.github/workflows/pull-project-tasks.yml.backup b/.github/workflows/pull-project-tasks.yml.backup new file mode 100644 index 0000000..df93de6 --- /dev/null +++ b/.github/workflows/pull-project-tasks.yml.backup @@ -0,0 +1,1029 @@ +name: pull-project-tasks + +on: + workflow_dispatch: # Allows manual triggering of the workflow + inputs: + source_owner: + description: 'The owner of the source repository' + required: true + default: 'im-infomagnus' + source_repo: + description: 'The name of the source repository' + required: true + default: 'ms-code-with-engineering-playbook' + target_owner: + description: 'The owner of the target repository' + required: true + default: '' + target_repo: + description: 'The name of the target repository' + required: true + default: '' + org_name: + description: 'The name of the GitHub Organization containing the project' + required: true + default: '' + project_name: + description: 'The name of the GitHub Project to update' + required: true + default: '' + pat_secret: + description: 'The name of the secret containing the PAT' + required: true + default: '' + +jobs: + update-project: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '16' + + - name: Authenticate with GitHub PAT + env: + GITHUB_TOKEN: ${{ secrets[inputs.pat_secret] }} + run: | + echo "Authenticated using PAT from secret: ${{ inputs.pat_secret }}" + + - name: Fetch Issues and Build Full Hierarchy + id: fetch_hierarchy + env: + GITHUB_TOKEN: ${{ secrets[inputs.pat_secret] }} + run: | + + # Create an error handler function for critical errors only + error_handler() { + local line=$1 + local command=$2 + echo "::error::Critical error occurred at line $line: Command '$command' failed" + } + # Rate limiting helper functions + check_rate_limit_rest() { + echo "Checking REST API rate limit..." + RATE_LIMIT_RESPONSE=$(gh api rate_limit) + REMAINING=$(echo "$RATE_LIMIT_RESPONSE" | jq -r '.rate.remaining') + RESET_TIME=$(echo "$RATE_LIMIT_RESPONSE" | jq -r '.rate.reset') + + echo "REST API requests remaining: $REMAINING" + + # Check if REMAINING is a valid number + if ! [[ "$REMAINING" =~ ^[0-9]+$ ]]; then + echo "Warning: Could not parse remaining rate limit, proceeding anyway" + return 0 + fi + + # Issue states rate limit check should pass if above 1000 + # Only sleep if we're running low on requests + + if [ "$REMAINING" -le 10 ]; then + CURRENT_TIME=$(date +%s) + SLEEP_TIME=$((RESET_TIME - CURRENT_TIME + 60)) + if [ "$SLEEP_TIME" -gt 0 ]; then + echo "Rate limit nearly exceeded. Sleeping for $SLEEP_TIME seconds..." + sleep "$SLEEP_TIME" + fi + fi + } + + check_rate_limit_graphql() { + echo "Checking GraphQL API rate limit..." + GRAPHQL_RATE_QUERY='query { rateLimit { remaining resetAt } }' + RATE_LIMIT_RESPONSE=$(gh api graphql -f query="$GRAPHQL_RATE_QUERY") + + if echo "$RATE_LIMIT_RESPONSE" | jq -e '.errors' >/dev/null 2>&1; then + echo "Warning: Could not check GraphQL rate limit" + return 0 + fi + + REMAINING=$(echo "$RATE_LIMIT_RESPONSE" | jq -r '.data.rateLimit.remaining') + RESET_AT=$(echo "$RATE_LIMIT_RESPONSE" | jq -r '.data.rateLimit.resetAt') + + echo "GraphQL API points remaining: $REMAINING" + + # Check if REMAINING is a valid number + if ! [[ "$REMAINING" =~ ^[0-9]+$ ]]; then + echo "Warning: Could not parse remaining rate limit, proceeding anyway" + return 0 + fi + + # Issue states rate limit check should pass if above 1000 + # Only sleep if we're running low on requests + + if [ "$REMAINING" -le 100 ]; then + RESET_EPOCH=$(date -d "$RESET_AT" +%s 2>/dev/null || echo "0") + CURRENT_TIME=$(date +%s) + SLEEP_TIME=$((RESET_EPOCH - CURRENT_TIME + 60)) + if [ "$SLEEP_TIME" -gt 0 ]; then + echo "GraphQL rate limit nearly exceeded. Sleeping for $SLEEP_TIME seconds..." + sleep "$SLEEP_TIME" + fi + fi + } + + retry_with_backoff() { + local max_attempts=5 + local delay=1 + local attempt=1 + + # Accept command as arguments, not as a single string + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt of $max_attempts: $*" + + # Run the command directly, not via eval + if "$@"; then + echo "Command succeeded on attempt $attempt." + return 0 + else + local exit_code=$? + echo "Command failed with exit code $exit_code on attempt $attempt." + + # Log the response from the command + local response + response=$("$@" 2>&1) + echo "Command response: $response" + + if [ $attempt -eq $max_attempts ]; then + echo "Max attempts reached. Command failed with exit code $exit_code." + return $exit_code + fi + + echo "Retrying in $delay seconds..." + sleep $delay + delay=$((delay * 2)) + attempt=$((attempt + 1)) + fi + done + } + + safe_gh_api() { + check_rate_limit_rest + retry_with_backoff gh api "$@" + } + + safe_gh_graphql() { + check_rate_limit_graphql + retry_with_backoff gh api graphql "$@" + } + + safe_curl() { + check_rate_limit_rest + retry_with_backoff curl "$@" + } + + # Initialize parent-child relationships file + echo "{}" > parent_child_links.json + + # Validate JSON after initialization + if ! jq empty parent_child_links.json; then + echo "Error: parent_child_links.json contains invalid JSON. Reinitializing..." + echo "{}" > parent_child_links.json + fi + + # Recursive function to fetch sub-issues and build hierarchy + fetch_subissues() { + local issue_number=$1 + local parent_number=$2 + + set +e # Disable immediate exit on error + echo "---------------------------------------------------------------------------" + echo "Processing issue #$issue_number" + + # Fetch sub-issues for the current issue + echo "Executing gh api graphql to fetch sub-issues for issue #$issue_number" + + # Fetch the Global ID + GET_ISSUE_ID_QUERY="query GetIssueId(\$owner: String!, \$repo: String!, \$issue_number: Int!) { repository(owner: \$owner, name: \$repo) { issue(number: \$issue_number) { id } } }" + echo "GraphQL query to fetch issue ID: $GET_ISSUE_ID_QUERY" + + # Validate that issue_number is a valid integer + if ! [[ "$issue_number" =~ ^[0-9]+$ ]]; then + echo "Error: issue_number '$issue_number' is not a valid integer" + return 1 + fi + + # Create a temporary file to capture error output + ERROR_OUTPUT=$(mktemp) + + # Convert issue_number to a clean integer + issue_number_int=$((10#$issue_number)) # Force base-10 interpretation + echo "Using integer value for issue_number: $issue_number_int" + + # Use alternative approach with direct curl to control JSON types properly + ISSUE_ID_RESPONSE=$(safe_curl -s -H "Authorization: bearer $GITHUB_TOKEN" \ + -X POST \ + -d "{\"query\":\"query { repository(owner: \\\"${{ inputs.source_owner }}\\\", name: \\\"${{ inputs.source_repo }}\\\") { issue(number: $issue_number_int) { id } } }\"}" \ + https://api.github.com/graphql 2>$ERROR_OUTPUT) + + # Check for curl errors + if [ $? -ne 0 ]; then + echo "Error executing GraphQL query: $(cat $ERROR_OUTPUT)" + return 1 + fi + + # Check for GraphQL errors in the response + if echo "$ISSUE_ID_RESPONSE" | jq -e '.errors' >/dev/null 2>&1; then + echo "GraphQL query returned errors:" + echo "$ISSUE_ID_RESPONSE" | jq '.errors' + return 1 + fi + + echo "GraphQL query executed successfully." + echo "Raw response: $ISSUE_ID_RESPONSE" + + ISSUE_ID=$(echo "$ISSUE_ID_RESPONSE" | jq -r '.data.repository.issue.id') + + echo "Issue ID: $ISSUE_ID" + + # Try multiple GraphQL queries to find sub-issues + echo "Executing GraphQL queries to fetch sub-issues for issue #$issue_number" + + # First try the original subIssues query + QUERY1="query(\$issue_id: ID!) { node(id: \$issue_id) { ... on Issue { id number subIssues(first: 100) { nodes { id number title url } } } } }" + echo "Trying subIssues field query: $QUERY1" + + set +e # Don't exit on error + safe_gh_graphql -f query="$QUERY1" -f issue_id="$ISSUE_ID" > subissues_response.json 2>/dev/null + QUERY1_SUCCESS=$? + set -e + + SUBISSUES="" + if [ $QUERY1_SUCCESS -eq 0 ] && ! jq -e '.errors' subissues_response.json > /dev/null 2>&1; then + echo "subIssues query successful" + if jq -e '.data.node.subIssues.nodes' subissues_response.json > /dev/null 2>&1; then + SUBISSUES=$(jq -c '.data.node.subIssues.nodes[]' subissues_response.json) + if [ -n "$SUBISSUES" ]; then + echo "Found sub-issues using subIssues field for issue #$issue_number:" + echo "$SUBISSUES" | jq -r '.number' + fi + fi + else + echo "subIssues query failed or returned errors, trying alternative approach" + fi + + # If subIssues didn't work, try timeline items and body parsing + 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" + + safe_gh_graphql -f query="$QUERY2" -f issue_id="$ISSUE_ID" > subissues_response.json + + # Check if the response contains any errors + if jq -e '.errors' subissues_response.json > /dev/null 2>&1; then + echo "GraphQL query returned errors:" + 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" + 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 + # 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/${{ inputs.target_owner }}/${{ inputs.target_repo }}/issues/$task_issue_num\"}" + if [ -z "$SUBISSUES" ]; then + SUBISSUES="$TASK_ISSUE_JSON" + else + SUBISSUES="$SUBISSUES\n$TASK_ISSUE_JSON" + fi + done + fi + fi + fi + fi + + if [ -z "$SUBISSUES" ]; then + echo "No sub-issues found for issue #$issue_number using any method" + else + echo "Total sub-issues found for issue #$issue_number:" + if command -v wc >/dev/null 2>&1; then + SUBISSUE_COUNT=$(echo -e "$SUBISSUES" | wc -l) + echo "Count: $SUBISSUE_COUNT" + fi + echo "Sub-issue details:" + echo -e "$SUBISSUES" | jq -r '.number' 2>/dev/null || echo -e "$SUBISSUES" + fi + + # # Check if response is empty first + # if [ ! -s subissues_response.json ]; then + # echo "No sub-issues found for issue #$issue_number (empty response)" + # return + # fi + + # # Check if response contains a 404 status + # if jq -e '.status == "404"' subissues_response.json > /dev/null; then + # echo "No sub-issues found for issue #$issue_number (404 status)" + # return + # fi + + # SUBISSUES=$(jq -c '.[]' subissues_response.json) + + # if [ -z "$SUBISSUES" ] || [ "$SUBISSUES" == "[]" ]; then + # echo "No sub-issues found for issue #$issue_number" + # return + # fi + + # Process sub-issues if any were found + if [ -n "$SUBISSUES" ]; then + # Validate JSON before processing + if ! jq empty parent_child_links.json; then + echo "Error: parent_child_links.json contains invalid JSON." + exit 1 + fi + + # Add the current issue to the hierarchy if not already present + if ! jq -e --arg issue "$issue_number" 'has($issue)' parent_child_links.json > /dev/null; then + jq --arg issue "$issue_number" '. + {($issue): []}' parent_child_links.json > temp.json \ + && mv temp.json parent_child_links.json + fi + + # Link sub-issues to the current issue + echo -e "$SUBISSUES" | while read -r subissue; do + if [ -n "$subissue" ]; then + SUBISSUE_NUMBER=$(echo "$subissue" | jq -r '.number') + SUBISSUE_URL=$(echo "$subissue" | jq -r '.url // "https://github.com/${{ inputs.target_owner }}/${{ inputs.target_repo }}/issues/\(.number)"') + + echo "Linking sub-issue #$SUBISSUE_NUMBER to parent issue #$issue_number" + + jq --arg parent "$issue_number" \ + --arg number "$SUBISSUE_NUMBER" \ + --arg url "$SUBISSUE_URL" \ + '.[$parent] += [{"number": $number, "url": $url}]' \ + parent_child_links.json > temp.json \ + && mv temp.json parent_child_links.json + fi + done + + echo "Contents of parent_child_links.json after processing issue #$issue_number:" + cat parent_child_links.json + fi + } + #end function + + # Enable error trapping only for critical errors + # Note: Most API calls handle errors gracefully, so we don't trap all ERR signals + set -e # Exit on error, but allow individual commands to handle their own errors + + TOP_LEVEL_ISSUES=$(mktemp) + PAGE=1 + PER_PAGE=100 + + echo "[]" > "$TOP_LEVEL_ISSUES" + + while : ; do + echo "Executing gh api command: gh api \"/repos/${{ inputs.source_owner }}/${{ inputs.source_repo }}/issues?state=open&per_page=$PER_PAGE&page=$PAGE\"" + RESPONSE=$(safe_gh_api "repos/${{ inputs.source_owner }}/${{ inputs.source_repo }}/issues?state=open&per_page=$PER_PAGE&page=$PAGE") + + #echo "Raw response for page $PAGE:" + #echo "$RESPONSE" + + # Check if response is empty + if [ -z "$RESPONSE" ]; then + echo "Warning: Empty response from GitHub API. Assuming end of pages." + break + fi + + # Check if response is valid JSON + if ! echo "$RESPONSE" | jq empty > /dev/null; then + echo "Warning: Invalid JSON response from GitHub API. Assuming end of pages. Response was:" + echo "$RESPONSE" + break + fi + + # Check if response is an empty array + if [ "$(echo "$RESPONSE" | jq '. | length')" -eq 0 ]; then + echo "No issues found on this page. Assuming end of pages." + break + fi + + # Append current page results to TOP_LEVEL_ISSUES + jq -s '.[0] + .[1]' "$TOP_LEVEL_ISSUES" <(echo "$RESPONSE") > "${TOP_LEVEL_ISSUES}.tmp" && mv "${TOP_LEVEL_ISSUES}.tmp" "$TOP_LEVEL_ISSUES" + + PAGE=$((PAGE + 1)) + done + + echo "Top-level issues:" + jq -r '.[].number' "$TOP_LEVEL_ISSUES" + echo "Total top-level issues found: $(jq '. | length' "$TOP_LEVEL_ISSUES")" + + # Log the contents of the JSON file before parsing + echo "Contents of parent_child_links.json before processing:" + cat parent_child_links.json + + #raw response for top-level issues + #echo "Raw response for top-level issues:" + #cat "$TOP_LEVEL_ISSUES" + + # Process each top-level issue + jq -c '.[]' "$TOP_LEVEL_ISSUES" | while read -r issue; do + # Check if $issue is empty or null + if [ -z "$issue" ] || [ "$issue" == "null" ]; then + echo "Warning: Skipping empty or null issue." + continue + fi + + # Check if $issue is valid JSON + if ! echo "$issue" | jq empty > /dev/null; then + echo "Warning: Invalid JSON for issue: $issue" + continue + fi + #echo "Issue: $issue" # Debugging statement + + # Check if the JSON object has a "number" field + if echo "$issue" | jq -e 'has("number")' > /dev/null; then + ISSUE_NUMBER=$(echo "$issue" | jq -r '.number') + else + echo "Warning: Issue does not have a 'number' field: $issue" + continue + fi + + + # Check if ISSUE_NUMBER is empty + if [ -z "$ISSUE_NUMBER" ]; then + echo "Warning: Issue number is empty for issue: $issue" + continue + fi + + echo "Processing top-level issue #$ISSUE_NUMBER" + fetch_subissues "$ISSUE_NUMBER" + done + + 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 + + - name: Copy Issues to Target Repository and Update URLs + id: copy_issues + env: + GITHUB_TOKEN: ${{ secrets[inputs.pat_secret] }} + run: | + # Rate limiting helper functions + check_rate_limit_rest() { + echo "Checking REST API rate limit..." + RATE_LIMIT_RESPONSE=$(gh api rate_limit) + REMAINING=$(echo "$RATE_LIMIT_RESPONSE" | jq -r '.rate.remaining') + RESET_TIME=$(echo "$RATE_LIMIT_RESPONSE" | jq -r '.rate.reset') + + echo "REST API requests remaining: $REMAINING" + + # Check if REMAINING is a valid number + if ! [[ "$REMAINING" =~ ^[0-9]+$ ]]; then + echo "Warning: Could not parse remaining rate limit, proceeding anyway" + return 0 + fi + + # Issue states rate limit check should pass if above 1000 + # Only sleep if we're running low on requests + + if [ "$REMAINING" -le 10 ]; then + CURRENT_TIME=$(date +%s) + SLEEP_TIME=$((RESET_TIME - CURRENT_TIME + 60)) + if [ "$SLEEP_TIME" -gt 0 ]; then + echo "Rate limit nearly exceeded. Sleeping for $SLEEP_TIME seconds..." + sleep "$SLEEP_TIME" + fi + fi + } + + retry_with_backoff() { + local max_attempts=5 + local delay=1 + local attempt=1 + + # Accept command as arguments, not as a single string + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt of $max_attempts: $*" + + # Run the command directly, not via eval + if "$@"; then + echo "Command succeeded on attempt $attempt." + return 0 + else + local exit_code=$? + echo "Command failed with exit code $exit_code on attempt $attempt." + + # Log the response from the command + local response + response=$("$@" 2>&1) + echo "Command response: $response" + + if [ $attempt -eq $max_attempts ]; then + echo "Max attempts reached. Command failed with exit code $exit_code." + return $exit_code + fi + + echo "Retrying in $delay seconds..." + sleep $delay + delay=$((delay * 2)) + attempt=$((attempt + 1)) + fi + done + } + + safe_gh_api() { + check_rate_limit_rest + retry_with_backoff gh api "$@" + } + + safe_curl() { + check_rate_limit_rest + retry_with_backoff curl "$@" + } + + # Function to replace URLs in issue body + replace_urls_in_text() { + local text="$1" + # Replace GitHub URLs pointing to source repo with target repo + echo "$text" | sed "s|https://github.com/${{ inputs.source_owner }}/${{ inputs.source_repo }}|https://github.com/${{ inputs.target_owner }}/${{ inputs.target_repo }}|g" + } + + # Initialize issue mapping file (maps source issue number to target issue number) + echo "{}" > issue_mapping.json + + # Get all issues that need to be copied + ALL_ISSUES_TO_COPY=$(mktemp) + echo "[]" > "$ALL_ISSUES_TO_COPY" + + # Add parent issues + for parent in $(jq -r 'keys_unsorted[]' parent_child_links.json); do + jq --arg issue "$parent" '. += [$issue]' "$ALL_ISSUES_TO_COPY" > "${ALL_ISSUES_TO_COPY}.tmp" && mv "${ALL_ISSUES_TO_COPY}.tmp" "$ALL_ISSUES_TO_COPY" + done + + # Add child issues + for parent in $(jq -r 'keys_unsorted[]' parent_child_links.json); do + for child in $(jq -r --arg parent "$parent" '.[$parent][].number' parent_child_links.json); do + # Check if child is not already in the list + if ! jq -e --arg child "$child" '. | contains([$child])' "$ALL_ISSUES_TO_COPY" > /dev/null; then + jq --arg issue "$child" '. += [$issue]' "$ALL_ISSUES_TO_COPY" > "${ALL_ISSUES_TO_COPY}.tmp" && mv "${ALL_ISSUES_TO_COPY}.tmp" "$ALL_ISSUES_TO_COPY" + fi + done + done + + echo "Issues to copy:" + cat "$ALL_ISSUES_TO_COPY" + + # Copy each issue to the target repository + for issue_number in $(jq -r '.[]' "$ALL_ISSUES_TO_COPY"); do + echo "Copying issue #$issue_number from ${{ inputs.source_owner }}/${{ inputs.source_repo }} to ${{ inputs.target_owner }}/${{ inputs.target_repo }}" + + # Fetch the original issue + ORIGINAL_ISSUE=$(safe_gh_api "repos/${{ inputs.source_owner }}/${{ inputs.source_repo }}/issues/$issue_number") + + if [ $? -ne 0 ] || [ -z "$ORIGINAL_ISSUE" ]; then + echo "Failed to fetch issue #$issue_number from source repository" + continue + fi + + ISSUE_TITLE=$(echo "$ORIGINAL_ISSUE" | jq -r '.title') + ISSUE_BODY=$(echo "$ORIGINAL_ISSUE" | jq -r '.body // ""') + ISSUE_LABELS=$(echo "$ORIGINAL_ISSUE" | jq -r '.labels[].name') + + # Replace URLs in the issue body + UPDATED_BODY=$(replace_urls_in_text "$ISSUE_BODY") + + # Add reference to original issue + UPDATED_BODY="$UPDATED_BODY"$'\n\n---\n'"*Copied from: https://github.com/${{ inputs.source_owner }}/${{ inputs.source_repo }}/issues/$issue_number*" + + # Create labels array for the API call + LABELS_JSON="[]" + if [ -n "$ISSUE_LABELS" ]; then + LABELS_JSON=$(echo "$ISSUE_LABELS" | jq -R -s 'split("\n") | map(select(length > 0))') + fi + + # Create the new issue in the target repository + NEW_ISSUE_DATA=$(jq -n \ + --arg title "$ISSUE_TITLE" \ + --arg body "$UPDATED_BODY" \ + --argjson labels "$LABELS_JSON" \ + '{title: $title, body: $body, labels: $labels}') + + echo "Creating issue with data: $NEW_ISSUE_DATA" + + NEW_ISSUE_RESPONSE=$(safe_curl -s -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Content-Type: application/json" \ + -d "$NEW_ISSUE_DATA" \ + "https://api.github.com/repos/${{ inputs.target_owner }}/${{ inputs.target_repo }}/issues") + + if [ $? -ne 0 ]; then + echo "Failed to create issue in target repository" + continue + fi + + NEW_ISSUE_NUMBER=$(echo "$NEW_ISSUE_RESPONSE" | jq -r '.number') + + if [ "$NEW_ISSUE_NUMBER" == "null" ] || [ -z "$NEW_ISSUE_NUMBER" ]; then + echo "Failed to get new issue number. Response: $NEW_ISSUE_RESPONSE" + continue + fi + + echo "Created new issue #$NEW_ISSUE_NUMBER in target repository" + + # Store the mapping + jq --arg source "$issue_number" --arg target "$NEW_ISSUE_NUMBER" \ + '. + {($source): $target}' issue_mapping.json > temp.json && mv temp.json issue_mapping.json + done + + echo "Issue mapping:" + cat issue_mapping.json + + - name: Add Issues to GitHub Project and Maintain Relationships + id: add_issues + env: + GITHUB_TOKEN: ${{ secrets[inputs.pat_secret] }} + run: | + # Rate limiting helper functions + check_rate_limit_rest() { + echo "Checking REST API rate limit..." + RATE_LIMIT_RESPONSE=$(gh api rate_limit) + REMAINING=$(echo "$RATE_LIMIT_RESPONSE" | jq -r '.rate.remaining') + RESET_TIME=$(echo "$RATE_LIMIT_RESPONSE" | jq -r '.rate.reset') + + echo "REST API requests remaining: $REMAINING" + + # Check if REMAINING is a valid number + if ! [[ "$REMAINING" =~ ^[0-9]+$ ]]; then + echo "Warning: Could not parse remaining rate limit, proceeding anyway" + return 0 + fi + + # Issue states rate limit check should pass if above 1000 + # Only sleep if we're running low on requests + + if [ "$REMAINING" -le 10 ]; then + CURRENT_TIME=$(date +%s) + SLEEP_TIME=$((RESET_TIME - CURRENT_TIME + 60)) + if [ "$SLEEP_TIME" -gt 0 ]; then + echo "Rate limit nearly exceeded. Sleeping for $SLEEP_TIME seconds..." + sleep "$SLEEP_TIME" + fi + fi + } + + check_rate_limit_graphql() { + echo "Checking GraphQL API rate limit..." + GRAPHQL_RATE_QUERY='query { rateLimit { remaining resetAt } }' + RATE_LIMIT_RESPONSE=$(gh api graphql -f query="$GRAPHQL_RATE_QUERY") + + if echo "$RATE_LIMIT_RESPONSE" | jq -e '.errors' >/dev/null 2>&1; then + echo "Warning: Could not check GraphQL rate limit" + return 0 + fi + + REMAINING=$(echo "$RATE_LIMIT_RESPONSE" | jq -r '.data.rateLimit.remaining') + RESET_AT=$(echo "$RATE_LIMIT_RESPONSE" | jq -r '.data.rateLimit.resetAt') + + echo "GraphQL API points remaining: $REMAINING" + + # Check if REMAINING is a valid number + if ! [[ "$REMAINING" =~ ^[0-9]+$ ]]; then + echo "Warning: Could not parse remaining rate limit, proceeding anyway" + return 0 + fi + + # Issue states rate limit check should pass if above 1000 + # Only sleep if we're running low on requests + + if [ "$REMAINING" -le 100 ]; then + RESET_EPOCH=$(date -d "$RESET_AT" +%s 2>/dev/null || echo "0") + CURRENT_TIME=$(date +%s) + SLEEP_TIME=$((RESET_EPOCH - CURRENT_TIME + 60)) + if [ "$SLEEP_TIME" -gt 0 ]; then + echo "GraphQL rate limit nearly exceeded. Sleeping for $SLEEP_TIME seconds..." + sleep "$SLEEP_TIME" + fi + fi + } + + retry_with_backoff() { + local max_attempts=5 + local delay=1 + local attempt=1 + + # Accept command as arguments, not as a single string + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt of $max_attempts: $*" + + # Run the command directly, not via eval + if "$@"; then + echo "Command succeeded on attempt $attempt." + return 0 + else + local exit_code=$? + echo "Command failed with exit code $exit_code on attempt $attempt." + + # Log the response from the command + local response + response=$("$@" 2>&1) + echo "Command response: $response" + + if [ $attempt -eq $max_attempts ]; then + echo "Max attempts reached. Command failed with exit code $exit_code." + return $exit_code + fi + + echo "Retrying in $delay seconds..." + sleep $delay + delay=$((delay * 2)) + attempt=$((attempt + 1)) + fi + done + } + safe_gh_api() { + check_rate_limit_rest + retry_with_backoff gh api "$@" + } + + safe_gh_graphql() { + check_rate_limit_graphql + retry_with_backoff gh api graphql "$@" + } + + safe_curl() { + check_rate_limit_rest + retry_with_backoff curl "$@" + } + + PROJECT_NAME="${{ inputs.project_name }}" + + # Fetch the Project ID using v1 API + echo "fetch the project ID" + ORG_NAME="${{ inputs.org_name }}" + PROJECT_NAME="${{ inputs.project_name }}" + + echo "Fetching project ID for project '$PROJECT_NAME' in organization '$ORG_NAME'" + + # Fetch the Project ID using GraphQL API for Projects v2 + GRAPHQL_QUERY='query($org: String!) { + organization(login: $org) { + projectsV2(first: 100) { + nodes { + id + title + } + } + } + }' + + API_RESPONSE=$(safe_gh_graphql -f query="$GRAPHQL_QUERY" -f org="$ORG_NAME") + + # Check if the API response is valid JSON + if ! echo "$API_RESPONSE" | jq empty > /dev/null 2>&1; then + echo "Error: Invalid JSON response from GitHub API:" + echo "$API_RESPONSE" + exit 1 + fi + + echo "API response: ${API_RESPONSE}" + + PROJECT_ID=$(echo "$API_RESPONSE" | jq -r ".data.organization.projectsV2.nodes[] | select(.title==\"$PROJECT_NAME\") | .id") + + if [ -z "$PROJECT_ID" ] || [ "$PROJECT_ID" == "null" ]; then + echo "Error: Project '$PROJECT_NAME' not found in organization '$ORG_NAME'." + exit 1 + fi + + echo "Project ID found: $PROJECT_ID" + + # Log the contents of the JSON file before adding issues + echo "Contents of parent_child_links.json before adding issues to the project:" + cat parent_child_links.json + echo "Contents of issue_mapping.json:" + cat issue_mapping.json + + # Add all issues to the project using the new issue numbers from target repository + for source_parent in $(jq -r 'keys_unsorted[]' parent_child_links.json); do + # Get the target issue number from mapping + target_parent=$(jq -r --arg source "$source_parent" '.[$source] // empty' issue_mapping.json) + + if [ -z "$target_parent" ] || [ "$target_parent" == "null" ]; then + echo "No target issue found for source issue #$source_parent, skipping" + continue + fi + + echo "Adding issue #$target_parent to the project (mapped from source #$source_parent)" + + # Convert target_parent to integer to ensure proper GraphQL typing + target_parent_int=$((10#$target_parent)) + + # Get the issue ID using GraphQL (Global ID needed for Projects v2) + ISSUE_QUERY='query($owner: String!, $repo: String!, $issueNumber: Int!) { + repository(owner: $owner, name: $repo) { + issue(number: $issueNumber) { + id + } + } + }' + + ISSUE_RESPONSE=$(safe_gh_graphql -f query="$ISSUE_QUERY" -f owner="${{ inputs.target_owner }}" -f repo="${{ inputs.target_repo }}" -F issueNumber="$target_parent_int") + + # Check for GraphQL errors in the response + if echo "$ISSUE_RESPONSE" | jq -e '.errors' >/dev/null 2>&1; then + echo "Error getting issue ID for #$target_parent:" + echo "$ISSUE_RESPONSE" | jq '.errors' + continue + fi + + ISSUE_ID=$(echo "$ISSUE_RESPONSE" | jq -r '.data.repository.issue.id') + + if [ -z "$ISSUE_ID" ] || [ "$ISSUE_ID" == "null" ]; then + echo "Failed to get Global ID for issue #$target_parent, skipping" + continue + fi + + echo "Issue Global ID: ${ISSUE_ID}" + + # Add the issue to the project using GraphQL mutation for Projects v2 + ADD_MUTATION='mutation($projectId: ID!, $contentId: ID!) { + addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) { + item { + id + } + } + }' + + ADD_RESPONSE=$(safe_gh_graphql -f query="$ADD_MUTATION" -f projectId="$PROJECT_ID" -f contentId="$ISSUE_ID") + + # Check for errors in adding to project + if echo "$ADD_RESPONSE" | jq -e '.errors' >/dev/null 2>&1; then + echo "Warning: Could not add issue #$target_parent to project:" + echo "$ADD_RESPONSE" | jq '.errors' + else + echo "Issue #$target_parent successfully added to the project" + fi + done + + # Log the contents of the JSON file before linking child issues + echo "add_issues: Contents of parent_child_links.json before linking child issues:" + cat parent_child_links.json + echo "Contents of issue_mapping.json:" + cat issue_mapping.json + + # Link child issues to their parents using target repository issue numbers + for source_parent in $(jq -r 'keys_unsorted[]' parent_child_links.json); do + target_parent=$(jq -r --arg source "$source_parent" '.[$source] // empty' issue_mapping.json) + + if [ -z "$target_parent" ] || [ "$target_parent" == "null" ]; then + echo "No target issue found for source parent #$source_parent, skipping" + continue + fi + + for child in $(jq -c --arg parent "$source_parent" '.[$parent][]' parent_child_links.json); do + SOURCE_CHILD_NUMBER=$(echo "$child" | jq -r '.number') + TARGET_CHILD_NUMBER=$(jq -r --arg source "$SOURCE_CHILD_NUMBER" '.[$source] // empty' issue_mapping.json) + + if [ -z "$TARGET_CHILD_NUMBER" ] || [ "$TARGET_CHILD_NUMBER" == "null" ]; then + echo "No target issue found for source child #$SOURCE_CHILD_NUMBER, skipping" + continue + fi + + echo "Linking child issue #$TARGET_CHILD_NUMBER to parent issue #$target_parent in target repository" + safe_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/${{ inputs.target_owner }}/${{ inputs.target_repo }}/issues/$TARGET_CHILD_NUMBER/comments" + echo "Child Issue #$TARGET_CHILD_NUMBER linked to Parent Issue #$target_parent" + done + done + + - name: Update Parent Issues with Child Links + env: + GITHUB_TOKEN: ${{ secrets[inputs.pat_secret] }} + run: | + # Rate limiting helper functions + check_rate_limit_rest() { + echo "Checking REST API rate limit..." + RATE_LIMIT_RESPONSE=$(gh api rate_limit) + REMAINING=$(echo "$RATE_LIMIT_RESPONSE" | jq -r '.rate.remaining') + RESET_TIME=$(echo "$RATE_LIMIT_RESPONSE" | jq -r '.rate.reset') + + echo "REST API requests remaining: $REMAINING" + + # Check if REMAINING is a valid number + if ! [[ "$REMAINING" =~ ^[0-9]+$ ]]; then + echo "Warning: Could not parse remaining rate limit, proceeding anyway" + return 0 + fi + + # Issue states rate limit check should pass if above 1000 + # Only sleep if we're running low on requests + + if [ "$REMAINING" -le 10 ]; then + CURRENT_TIME=$(date +%s) + SLEEP_TIME=$((RESET_TIME - CURRENT_TIME + 60)) + if [ "$SLEEP_TIME" -gt 0 ]; then + echo "Rate limit nearly exceeded. Sleeping for $SLEEP_TIME seconds..." + sleep "$SLEEP_TIME" + fi + fi + } + + retry_with_backoff() { + local max_attempts=5 + local delay=1 + local attempt=1 + + # Accept command as arguments, not as a single string + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt of $max_attempts: $*" + + # Run the command directly, not via eval + if "$@"; then + echo "Command succeeded on attempt $attempt." + return 0 + else + local exit_code=$? + echo "Command failed with exit code $exit_code on attempt $attempt." + + # Log the response from the command + local response + response=$("$@" 2>&1) + echo "Command response: $response" + + if [ $attempt -eq $max_attempts ]; then + echo "Max attempts reached. Command failed with exit code $exit_code." + return $exit_code + fi + + echo "Retrying in $delay seconds..." + sleep $delay + delay=$((delay * 2)) + attempt=$((attempt + 1)) + fi + done + } + + safe_curl() { + check_rate_limit_rest + retry_with_backoff curl "$@" + } + + # Log the contents of the JSON file before updating parent issues + echo "Contents of parent_child_links.json before updating parent issues:" + cat parent_child_links.json + echo "Contents of issue_mapping.json:" + cat issue_mapping.json + + for source_parent in $(jq -r 'keys_unsorted[]' parent_child_links.json); do + target_parent=$(jq -r --arg source "$source_parent" '.[$source] // empty' issue_mapping.json) + + if [ -z "$target_parent" ] || [ "$target_parent" == "null" ]; then + echo "No target issue found for source parent #$source_parent, skipping" + continue + fi + + # Build child links using target issue numbers and URLs + CHILD_LINKS="" + for child in $(jq -c --arg parent "$source_parent" '.[$parent][]' parent_child_links.json); do + SOURCE_CHILD_NUMBER=$(echo "$child" | jq -r '.number') + TARGET_CHILD_NUMBER=$(jq -r --arg source "$SOURCE_CHILD_NUMBER" '.[$source] // empty' issue_mapping.json) + + if [ -n "$TARGET_CHILD_NUMBER" ] && [ "$TARGET_CHILD_NUMBER" != "null" ]; then + CHILD_URL="https://github.com/${{ inputs.target_owner }}/${{ inputs.target_repo }}/issues/$TARGET_CHILD_NUMBER" + if [ -z "$CHILD_LINKS" ]; then + CHILD_LINKS="- [Issue #$TARGET_CHILD_NUMBER]($CHILD_URL)" + else + CHILD_LINKS="$CHILD_LINKS\n- [Issue #$TARGET_CHILD_NUMBER]($CHILD_URL)" + fi + fi + done + + if [ -n "$CHILD_LINKS" ]; then + echo "Updating parent issue #$target_parent with child links" + safe_curl -s -X PATCH \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + -d "{\"body\": \"# Linked Child Issues\n$CHILD_LINKS\"}" \ + "https://api.github.com/repos/${{ inputs.target_owner }}/${{ inputs.target_repo }}/issues/$target_parent" + echo "Parent Issue #$target_parent updated with child links." + fi + done + echo "All parent issues updated with child links." \ No newline at end of file