From 102172687309c7c6f768f926d943f41abe4827a5 Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Thu, 14 Aug 2025 14:01:58 -0500 Subject: [PATCH] feat: add script to extract items from Projects V2 board --- gh-cli/README.md | 20 +++ gh-cli/get-project-board-items.sh | 281 ++++++++++++++++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100755 gh-cli/get-project-board-items.sh diff --git a/gh-cli/README.md b/gh-cli/README.md index b2455f0..1de05b4 100644 --- a/gh-cli/README.md +++ b/gh-cli/README.md @@ -1021,6 +1021,26 @@ Retrieve the download URL for a specific version of a package in GitHub Packages Gets the parent issue of a given sub-issue (child). See: [Community Discussions Post](https://github.com/orgs/community/discussions/139932) +### get-project-board-items.sh + +Extracts all items from a GitHub Projects V2 board with comprehensive details including content, custom field values, and project item type (draft or issue). + +Usage: + +```shell +./get-project-board-items.sh my-org 123 +``` + +The script outputs formatted information for each project item including: + +- Issue/PR details with repository links and numbers +- Draft issue content +- Custom field values (Status, Priority, etc.) +- Labels and descriptions with clean formatting + +> [!NOTE] +> Works with Projects V2 (newer project boards). Find the project number in the URL: `github.com/orgs/ORG/projects/NUMBER` + ### get-projects-added-to-repository.sh Gets ProjectsV2 added to a repository diff --git a/gh-cli/get-project-board-items.sh b/gh-cli/get-project-board-items.sh new file mode 100755 index 0000000..7ebe64c --- /dev/null +++ b/gh-cli/get-project-board-items.sh @@ -0,0 +1,281 @@ +#!/bin/bash + +# Extract project board cards and descriptions using GraphQL +# This script works with GitHub Projects V2 (the newer project boards) +# Usage: ./get-project-board-items.sh + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + echo "Example: ./get-project-board-items.sh my-org 123" + echo "" + echo "Note: This script works with Projects V2 (the newer project boards)" + echo "To find project number, check the URL: github.com/orgs/ORG/projects/NUMBER" + exit 1 +fi + +org="$1" +project_number="$2" + +echo "🔍 Fetching project board items for project #$project_number in $org..." +echo "" + +# GraphQL query to get project items with their content and field values +response=$(gh api graphql --paginate -f org="$org" -F projectNumber="$project_number" -f query=' + query($org: String!, $projectNumber: Int!, $endCursor: String) { + organization(login: $org) { + projectV2(number: $projectNumber) { + title + id + items(first: 100, after: $endCursor) { + nodes { + id + content { + __typename + ... on Issue { + title + body + number + url + repository { + name + owner { + login + } + } + labels(first: 10) { + nodes { + name + } + } + } + ... on PullRequest { + title + body + number + url + repository { + name + owner { + login + } + } + } + ... on DraftIssue { + title + body + } + } + fieldValues(first: 20) { + nodes { + ... on ProjectV2ItemFieldTextValue { + text + field { + ... on ProjectV2FieldCommon { + name + } + } + } + ... on ProjectV2ItemFieldSingleSelectValue { + name + field { + ... on ProjectV2FieldCommon { + name + } + } + } + ... on ProjectV2ItemFieldIterationValue { + title + field { + ... on ProjectV2FieldCommon { + name + } + } + } + ... on ProjectV2ItemFieldDateValue { + date + field { + ... on ProjectV2FieldCommon { + name + } + } + } + ... on ProjectV2ItemFieldNumberValue { + number + field { + ... on ProjectV2FieldCommon { + name + } + } + } + } + } + } + pageInfo { + endCursor + hasNextPage + } + } + } + } + } +' 2>&1) + +# Check for errors +if [ $? -ne 0 ]; then + if echo "$response" | grep -q "Could not resolve to a ProjectV2"; then + echo "❌ Error: Project #$project_number not found in organization '$org'" + echo "Make sure:" + echo "- The project number is correct" + echo "- The project exists in the organization (not user-owned)" + echo "- You have access to view the project" + exit 1 + elif echo "$response" | grep -q "403\|Forbidden"; then + echo "❌ Error: Access denied to project #$project_number" + echo "Make sure you have permission to view this project" + exit 1 + else + echo "❌ Error fetching project data:" + echo "$response" + exit 1 + fi +fi + +# Extract project title +project_title=$(echo "$response" | jq -r '.data.organization.projectV2.title // "Unknown Project"') +echo "📋 Project: $project_title" +echo "===============================================" +echo "" + +# Process items +items=$(echo "$response" | jq -c '.data.organization.projectV2.items.nodes[]?') + +if [ -z "$items" ]; then + echo "â„šī¸ No items found in this project board" + exit 0 +fi + +item_count=0 +echo "$items" | while IFS= read -r item; do + ((item_count++)) + + # Extract content details + content_type=$(echo "$item" | jq -r '.content.__typename // "Unknown"') + + # If __typename is Unknown but we have content, determine type from content structure + if [ "$content_type" = "Unknown" ]; then + # Check if it has repository info and number - it's a GitHub Issue + if echo "$item" | jq -e '.content.repository.name and .content.number' >/dev/null 2>&1; then + content_type="Issue" + # Check if it has title and body but no repository - it's a Draft Issue + elif echo "$item" | jq -e '.content.title and (.content.repository | not)' >/dev/null 2>&1; then + content_type="DraftIssue" + # If content is null/empty, it's a standalone project item + elif [ "$(echo "$item" | jq -r '.content')" = "null" ] || [ -z "$(echo "$item" | jq -r '.content.title // empty')" ]; then + content_type="ProjectItem" + fi + fi + + # Handle different content types appropriately + if [ "$content_type" = "ProjectItem" ]; then + title=$(echo "$item" | jq -r '.fieldValues.nodes[] | select(.field.name == "Title") | .text // empty') + if [ -z "$title" ]; then + title="No title" + fi + # Try to get body/description from custom fields + body=$(echo "$item" | jq -r '.fieldValues.nodes[] | select(.field.name == "Description" or .field.name == "Body") | .text // empty') + number="" + url="" + repo_name="" + repo_owner="" + else + title=$(echo "$item" | jq -r '.content.title // "No title"') + body=$(echo "$item" | jq -r '.content.body // ""') + number=$(echo "$item" | jq -r '.content.number // ""') + url=$(echo "$item" | jq -r '.content.url // ""') + repo_name=$(echo "$item" | jq -r '.content.repository.name // ""') + repo_owner=$(echo "$item" | jq -r '.content.repository.owner.login // ""') + fi + + # Format item header + echo "🔖 Item #$item_count" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + case $content_type in + "Issue") + echo "īŋŊ Type: GitHub Issue" + echo "📍 Repository: $repo_owner/$repo_name" + echo "đŸ”ĸ Number: #$number" + echo "🌐 URL: $url" + ;; + "PullRequest") + echo "🔀 Type: Pull Request" + echo "📍 Repository: $repo_owner/$repo_name" + echo "đŸ”ĸ Number: #$number" + echo "🌐 URL: $url" + ;; + "DraftIssue") + echo "📝 Type: Draft Issue (project-only)" + ;; + "ProjectItem") + echo "đŸŽ¯ Type: Standalone Project Card" + ;; + *) + echo "❓ Type: $content_type" + ;; + esac + + echo "📰 Title: $title" + + # Show description if it exists + if [ -n "$body" ] && [ "$body" != "null" ] && [ "$body" != "" ]; then + echo "" + echo "📄 Description:" + echo "┌─────────────────────────────────────────────────" + echo "$body" | sed 's/^/│ /' + echo "└─────────────────────────────────────────────────" + fi + + # Show labels for issues + if [ "$content_type" = "Issue" ]; then + labels=$(echo "$item" | jq -r '.content.labels.nodes[]?.name // empty' | tr '\n' ' ') + if [ -n "$labels" ]; then + echo "" + echo "đŸˇī¸ Labels: $labels" + fi + fi + + # Show custom field values + field_values=$(echo "$item" | jq -c '.fieldValues.nodes[]? | select(.field.name != null and .field.name != "Title" and .field.name != "Description" and .field.name != "Body")') + if [ -n "$field_values" ]; then + echo "" + echo "📊 Custom Fields:" + echo "$field_values" | while IFS= read -r field_value; do + field_name=$(echo "$field_value" | jq -r '.field.name') + value="" + + # Extract value based on field type + if echo "$field_value" | jq -e '.text' >/dev/null 2>&1; then + value=$(echo "$field_value" | jq -r '.text') + elif echo "$field_value" | jq -e '.name' >/dev/null 2>&1; then + value=$(echo "$field_value" | jq -r '.name') + elif echo "$field_value" | jq -e '.title' >/dev/null 2>&1; then + value=$(echo "$field_value" | jq -r '.title') + elif echo "$field_value" | jq -e '.date' >/dev/null 2>&1; then + value=$(echo "$field_value" | jq -r '.date') + elif echo "$field_value" | jq -e '.number' >/dev/null 2>&1; then + value=$(echo "$field_value" | jq -r '.number') + fi + + if [ -n "$value" ] && [ "$value" != "null" ]; then + echo " â€ĸ $field_name: $value" + fi + done + fi + + echo "" + echo "" +done + +# Count total items +total_items=$(echo "$items" | wc -l | tr -d ' ') +echo "📊 Summary: Found $total_items items in project '$project_title'"