Skip to content

Commit af595df

Browse files
feat: add script to extract items from Projects V2 board (#111)
1 parent 6208808 commit af595df

File tree

2 files changed

+301
-0
lines changed

2 files changed

+301
-0
lines changed

gh-cli/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,26 @@ Retrieve the download URL for a specific version of a package in GitHub Packages
10211021

10221022
Gets the parent issue of a given sub-issue (child). See: [Community Discussions Post](https://github.com/orgs/community/discussions/139932)
10231023

1024+
### get-project-board-items.sh
1025+
1026+
Extracts all items from a GitHub Projects V2 board with comprehensive details including content, custom field values, and project item type (draft or issue).
1027+
1028+
Usage:
1029+
1030+
```shell
1031+
./get-project-board-items.sh my-org 123
1032+
```
1033+
1034+
The script outputs formatted information for each project item including:
1035+
1036+
- Issue/PR details with repository links and numbers
1037+
- Draft issue content
1038+
- Custom field values (Status, Priority, etc.)
1039+
- Labels and descriptions with clean formatting
1040+
1041+
> [!NOTE]
1042+
> Works with Projects V2 (newer project boards). Find the project number in the URL: `github.com/orgs/ORG/projects/NUMBER`
1043+
10241044
### get-projects-added-to-repository.sh
10251045

10261046
Gets ProjectsV2 added to a repository

gh-cli/get-project-board-items.sh

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
#!/bin/bash
2+
3+
# Extract project board cards and descriptions using GraphQL
4+
# This script works with GitHub Projects V2 (the newer project boards)
5+
# Usage: ./get-project-board-items.sh <org> <project-number>
6+
7+
if [ $# -ne 2 ]; then
8+
echo "Usage: $0 <org> <project-number>"
9+
echo "Example: ./get-project-board-items.sh my-org 123"
10+
echo ""
11+
echo "Note: This script works with Projects V2 (the newer project boards)"
12+
echo "To find project number, check the URL: github.com/orgs/ORG/projects/NUMBER"
13+
exit 1
14+
fi
15+
16+
org="$1"
17+
project_number="$2"
18+
19+
echo "🔍 Fetching project board items for project #$project_number in $org..."
20+
echo ""
21+
22+
# GraphQL query to get project items with their content and field values
23+
response=$(gh api graphql --paginate -f org="$org" -F projectNumber="$project_number" -f query='
24+
query($org: String!, $projectNumber: Int!, $endCursor: String) {
25+
organization(login: $org) {
26+
projectV2(number: $projectNumber) {
27+
title
28+
id
29+
items(first: 100, after: $endCursor) {
30+
nodes {
31+
id
32+
content {
33+
__typename
34+
... on Issue {
35+
title
36+
body
37+
number
38+
url
39+
repository {
40+
name
41+
owner {
42+
login
43+
}
44+
}
45+
labels(first: 10) {
46+
nodes {
47+
name
48+
}
49+
}
50+
}
51+
... on PullRequest {
52+
title
53+
body
54+
number
55+
url
56+
repository {
57+
name
58+
owner {
59+
login
60+
}
61+
}
62+
}
63+
... on DraftIssue {
64+
title
65+
body
66+
}
67+
}
68+
fieldValues(first: 20) {
69+
nodes {
70+
... on ProjectV2ItemFieldTextValue {
71+
text
72+
field {
73+
... on ProjectV2FieldCommon {
74+
name
75+
}
76+
}
77+
}
78+
... on ProjectV2ItemFieldSingleSelectValue {
79+
name
80+
field {
81+
... on ProjectV2FieldCommon {
82+
name
83+
}
84+
}
85+
}
86+
... on ProjectV2ItemFieldIterationValue {
87+
title
88+
field {
89+
... on ProjectV2FieldCommon {
90+
name
91+
}
92+
}
93+
}
94+
... on ProjectV2ItemFieldDateValue {
95+
date
96+
field {
97+
... on ProjectV2FieldCommon {
98+
name
99+
}
100+
}
101+
}
102+
... on ProjectV2ItemFieldNumberValue {
103+
number
104+
field {
105+
... on ProjectV2FieldCommon {
106+
name
107+
}
108+
}
109+
}
110+
}
111+
}
112+
}
113+
pageInfo {
114+
endCursor
115+
hasNextPage
116+
}
117+
}
118+
}
119+
}
120+
}
121+
' 2>&1)
122+
123+
# Check for errors
124+
if [ $? -ne 0 ]; then
125+
if echo "$response" | grep -q "Could not resolve to a ProjectV2"; then
126+
echo "❌ Error: Project #$project_number not found in organization '$org'"
127+
echo "Make sure:"
128+
echo "- The project number is correct"
129+
echo "- The project exists in the organization (not user-owned)"
130+
echo "- You have access to view the project"
131+
exit 1
132+
elif echo "$response" | grep -q "403\|Forbidden"; then
133+
echo "❌ Error: Access denied to project #$project_number"
134+
echo "Make sure you have permission to view this project"
135+
exit 1
136+
else
137+
echo "❌ Error fetching project data:"
138+
echo "$response"
139+
exit 1
140+
fi
141+
fi
142+
143+
# Extract project title
144+
project_title=$(echo "$response" | jq -r '.data.organization.projectV2.title // "Unknown Project"')
145+
echo "📋 Project: $project_title"
146+
echo "==============================================="
147+
echo ""
148+
149+
# Process items
150+
items=$(echo "$response" | jq -c '.data.organization.projectV2.items.nodes[]?')
151+
152+
if [ -z "$items" ]; then
153+
echo "ℹ️ No items found in this project board"
154+
exit 0
155+
fi
156+
157+
item_count=0
158+
echo "$items" | while IFS= read -r item; do
159+
((item_count++))
160+
161+
# Extract content details
162+
content_type=$(echo "$item" | jq -r '.content.__typename // "Unknown"')
163+
164+
# If __typename is Unknown but we have content, determine type from content structure
165+
if [ "$content_type" = "Unknown" ]; then
166+
# Check if it has repository info and number - it's a GitHub Issue
167+
if echo "$item" | jq -e '.content.repository.name and .content.number' >/dev/null 2>&1; then
168+
content_type="Issue"
169+
# Check if it has title and body but no repository - it's a Draft Issue
170+
elif echo "$item" | jq -e '.content.title and (.content.repository | not)' >/dev/null 2>&1; then
171+
content_type="DraftIssue"
172+
# If content is null/empty, it's a standalone project item
173+
elif [ "$(echo "$item" | jq -r '.content')" = "null" ] || [ -z "$(echo "$item" | jq -r '.content.title // empty')" ]; then
174+
content_type="ProjectItem"
175+
fi
176+
fi
177+
178+
# Handle different content types appropriately
179+
if [ "$content_type" = "ProjectItem" ]; then
180+
title=$(echo "$item" | jq -r '.fieldValues.nodes[] | select(.field.name == "Title") | .text // empty')
181+
if [ -z "$title" ]; then
182+
title="No title"
183+
fi
184+
# Try to get body/description from custom fields
185+
body=$(echo "$item" | jq -r '.fieldValues.nodes[] | select(.field.name == "Description" or .field.name == "Body") | .text // empty')
186+
number=""
187+
url=""
188+
repo_name=""
189+
repo_owner=""
190+
else
191+
title=$(echo "$item" | jq -r '.content.title // "No title"')
192+
body=$(echo "$item" | jq -r '.content.body // ""')
193+
number=$(echo "$item" | jq -r '.content.number // ""')
194+
url=$(echo "$item" | jq -r '.content.url // ""')
195+
repo_name=$(echo "$item" | jq -r '.content.repository.name // ""')
196+
repo_owner=$(echo "$item" | jq -r '.content.repository.owner.login // ""')
197+
fi
198+
199+
# Format item header
200+
echo "🔖 Item #$item_count"
201+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
202+
203+
case $content_type in
204+
"Issue")
205+
echo "� Type: GitHub Issue"
206+
echo "📍 Repository: $repo_owner/$repo_name"
207+
echo "🔢 Number: #$number"
208+
echo "🌐 URL: $url"
209+
;;
210+
"PullRequest")
211+
echo "🔀 Type: Pull Request"
212+
echo "📍 Repository: $repo_owner/$repo_name"
213+
echo "🔢 Number: #$number"
214+
echo "🌐 URL: $url"
215+
;;
216+
"DraftIssue")
217+
echo "📝 Type: Draft Issue (project-only)"
218+
;;
219+
"ProjectItem")
220+
echo "🎯 Type: Standalone Project Card"
221+
;;
222+
*)
223+
echo "❓ Type: $content_type"
224+
;;
225+
esac
226+
227+
echo "📰 Title: $title"
228+
229+
# Show description if it exists
230+
if [ -n "$body" ] && [ "$body" != "null" ] && [ "$body" != "" ]; then
231+
echo ""
232+
echo "📄 Description:"
233+
echo "┌─────────────────────────────────────────────────"
234+
echo "$body" | sed 's/^/│ /'
235+
echo "└─────────────────────────────────────────────────"
236+
fi
237+
238+
# Show labels for issues
239+
if [ "$content_type" = "Issue" ]; then
240+
labels=$(echo "$item" | jq -r '.content.labels.nodes[]?.name // empty' | tr '\n' ' ')
241+
if [ -n "$labels" ]; then
242+
echo ""
243+
echo "🏷️ Labels: $labels"
244+
fi
245+
fi
246+
247+
# Show custom field values
248+
field_values=$(echo "$item" | jq -c '.fieldValues.nodes[]? | select(.field.name != null and .field.name != "Title" and .field.name != "Description" and .field.name != "Body")')
249+
if [ -n "$field_values" ]; then
250+
echo ""
251+
echo "📊 Custom Fields:"
252+
echo "$field_values" | while IFS= read -r field_value; do
253+
field_name=$(echo "$field_value" | jq -r '.field.name')
254+
value=""
255+
256+
# Extract value based on field type
257+
if echo "$field_value" | jq -e '.text' >/dev/null 2>&1; then
258+
value=$(echo "$field_value" | jq -r '.text')
259+
elif echo "$field_value" | jq -e '.name' >/dev/null 2>&1; then
260+
value=$(echo "$field_value" | jq -r '.name')
261+
elif echo "$field_value" | jq -e '.title' >/dev/null 2>&1; then
262+
value=$(echo "$field_value" | jq -r '.title')
263+
elif echo "$field_value" | jq -e '.date' >/dev/null 2>&1; then
264+
value=$(echo "$field_value" | jq -r '.date')
265+
elif echo "$field_value" | jq -e '.number' >/dev/null 2>&1; then
266+
value=$(echo "$field_value" | jq -r '.number')
267+
fi
268+
269+
if [ -n "$value" ] && [ "$value" != "null" ]; then
270+
echo "$field_name: $value"
271+
fi
272+
done
273+
fi
274+
275+
echo ""
276+
echo ""
277+
done
278+
279+
# Count total items
280+
total_items=$(echo "$items" | wc -l | tr -d ' ')
281+
echo "📊 Summary: Found $total_items items in project '$project_title'"

0 commit comments

Comments
 (0)