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 1000if you are in a large org 
 - run 
 - you need to know the new field id (new field being 
CARD_IDin this case), which will look likePVTF_XXXXXXXXXX- run 
gh project field-list 123where123is 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.

