Assign numbers/identifiers to your GitHub Project cards
github niche
Assign numbers/identifiers to your GitHub Project cards
Problem statement: When using GitHub project board, created cards do NOT have an intrinsic number/identifier (sure, there is technically an itemId
in the URL which consists of 8 digits, but that is not human identifiable and does not exist visually on the cards themselves). They get assigned a number once they become an issue associated with a particular repo. This is problematic if your project involves multiple repos.
This is what it looks like before this tutorial:
This is what it will look like after this tutorial:
To reiterate, this is most relevant to projects with multiple repos.
Prerequisites
- you need to know your project id, which will look like
PVT_XXXXXXXXXX
- run
gh project list
, optionally append--limit 1000
if you are in a large org
- run
- you need to know the new field id (new field being
CARD_ID
in this case), which will look likePVTF_XXXXXXXXXX
- run
gh project field-list 123
where123
is your project number which you can find in your project’s URL
- run
- you need a GitHub PAT with the following scopes:
project
,read:org
,repo
Python script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#!/usr/bin/env python3
import requests
import time
import os
GITHUB_TOKEN = "ghp_XXXXXXXXXX"
PROJECT_ID = "PVT_XXXXXXXXXX"
FIELD_ID = "PVTF_XXXXXXXXXX"
def run_graphql(query, variables=None):
"""Execute a GraphQL query against GitHub API"""
headers = {
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Content-Type": "application/json",
}
payload = {"query": query}
if variables:
payload["variables"] = variables
response = requests.post(
"https://api.github.com/graphql",
headers=headers,
json=payload
)
response.raise_for_status()
return response.json()
def get_all_project_items():
"""Get all items from the project with pagination"""
all_items = []
has_next_page = True
cursor = None
page = 1
while has_next_page:
cursor_param = f', after: "{cursor}"' if cursor else ''
query = f"""
query {{
node(id: "{PROJECT_ID}") {{
... on ProjectV2 {{
title
items(first: 100{cursor_param}) {{
nodes {{
id
fieldValues(first: 100) {{
nodes {{
... on ProjectV2ItemFieldNumberValue {{
field {{
... on ProjectV2FieldCommon {{
id
}}
}}
number
}}
}}
}}
}}
pageInfo {{
hasNextPage
endCursor
}}
}}
}}
}}
}}
"""
r = run_graphql(query)
items = result["data"]["node"]["items"]["nodes"]
all_items.extend(items)
page_info = result["data"]["node"]["items"]["pageInfo"]
has_next_page = page_info["hasNextPage"]
cursor = page_info["endCursor"]
print(f"Page {page}: Fetched {len(items)} items (total: {len(all_items)})")
if has_next_page:
time.sleep(0.5) # Small delay to be nice to GitHub's API
return all_items
def filter_out_items_with_id(all_items):
"""Find items without numbers and assign them"""
# Find highest number and items without numbers
highest_number = 0
items_without_id = []
for item in all_items:
has_number = False
for field_value in item["fieldValues"]["nodes"]:
if (field_value and
field_value.get("field") and
field_value["field"].get("id") == FIELD_ID):
has_number = True
if field_value["number"] > highest_number:
highest_number = field_value["number"]
if not has_number:
items_without_id.append(item["id"])
print(f"Found {len(all_items)} total items")
print(f"Found {len(items_without_id)} items without numbers")
print(f"Highest existing number: {highest_number}")
return highest_number, items_without_id
def assign_id(highest_number, items_without_id):
"""Find a number to each id-less item"""
for item_id in items_without_id:
highest_number += + 1.0 # NOTE: this must be a float if you picked 'number'
mutation = f"""
mutation {{
updateProjectV2ItemFieldValue(
input: {{
projectId: "{PROJECT_ID}"
itemId: "{item_id}"
fieldId: "{FIELD_ID}"
value: {{
number: {highest_number}
}}
}}
) {{
projectV2Item {{
id
}}
}}
}}
"""
r = run_graphql(mutation)
time.sleep(0.5) # Small delay between updates
print(f"Finished! Assigned numbers to {len(items_without_id)} items")
def main():
all_items = get_all_project_items()
highest_number, filtered_items = filter_out_items_with_id(all_items)
assign_id(highest_number, filtered_items)
if __name__ == "__main__":
main()
Final remarks
You may include this script as part of a github action that runs as frequently as you want to keep your board up to date!
This post is licensed under CC BY 4.0 by the author.