Blogs· 5min February 22, 2023
Good question! Well, the GitHub classic to v2 migration tool migrates issues to a v2 board with the same status names/columns as the classic board. If you want to migrate the issues on this new board into a different v2 board, or you just want to combine multiple v2 project boards into one, you'll need to work outside of the GitHub UI.
In this blog post, I'm going to show you how I did this using a combination of:
This can be achieved with the GitHub GraphQL API, via the GitHub CLI:
gh api graphql \
--paginate \
--jq '.data.organization.projectV2.items.nodes[]' \
-f query='
query($endCursor: String) {
organization(login:"<organisation-id>"){
projectV2(number:<project-number>){
items(first:10, after: $endCursor){
pageInfo{ hasNextPage endCursor }
nodes{
fieldValueByName(name:"Status"){
__typename
... on ProjectV2ItemFieldSingleSelectValue{
name
}
}
content{
__typename
... on Issue{
number
title
id
}
}
}
}
}
}
}' | jq | sed 's/^}$/},/g' | sed '1s/^{$/[{/g' | sed '$s/^},$/}]/g' | tee issues.json
This results in some output like this:
[{
"content": {
"__typename": "Issue",
"id": "I_kwDOGolhjhjkhjke",
"number": 112,
"title": "Handle thing field correctly in blah"
},
"fieldValueByName": {
"__typename": "ProjectV2ItemFieldSingleSelectValue",
"name": "Done 🎉"
}
},
{
"content": {
"__typename": "Issue",
"id": "I_kwDOGhjkhkjhjkkN",
"number": 234,
"title": "Add a stage to the CI which runs the load tests"
},
"fieldValueByName": {
"__typename": "ProjectV2ItemFieldSingleSelectValue",
"name": "Done 🎉"
}
}]
Let's just examine the commands in the pipeline:
The result is a nicely formatted JSON array containing an element for every issue on the board, along with its current status.
Now that we've got all the issues and their current statuses, we can map all their statuses to statuses on your new project board. This is the step that allows you to migrate issues from one board to another, when there isn't necessarily a straightforward mapping from the statuses on one board to the other.
This is hard to do in Bash, but relatively straight forward in Python:
import json
import os
import sys
# This function maps statuses on your source board to those on your
# target board.
def map_old_status_to_new(old_status):
if "Backlog" in old_status:
return "Product planning"
if "Ready To Start" in old_status:
return "Ready to develop"
if "In Progress" in old_status:
return "In development"
if "Done" in old_status:
return None
if "Parked" in old_status:
return None
# Read the issues data from file.
with open('issues.json') as f:
issues = json.loads(f.read())
# Iterate over each of the issues, and map the old status to their
# new status.
for issue in issues:
try:
number = issue["content"]["number"]
old_status = issue["fieldValueByName"]["name"]
new_status = map_old_status_to_new(old_status)
if new_status is None:
print(
"ignoring issue {} because its status is {}".format(
number,
old_status
),
file=sys.stderr
)
continue
# Output some new JSON representing the issue and its mapped status.
mapped_issue = {
"number": number,
"id": issue["content"]["id"],
"old_status": old_status,
"new_status": new_status
}
print(json.dumps(mapped_issue))
except Exception as e:
print(
"error processing issue. error: {}, issue: {}".format(e, issue),
file=sys.stderr
)
This script reads your issues data, and re-shapes the data into a JSON object per line of standard output. Each object contains the information required to add the issue to the new project board in the correct status.
You could run this Python script from a shell and pipe its output to subsequent commands, or save the output to a file.
The output looks something like this:
{"number": 1151, "id": "I_kwhjkol6Is5ZStTQ", "old_status": "In Progress \ud83c\udfd7\ufe0f", "new_status": "In development"}
{"number": 902, "id": "I_kwhjkol6Is5U9_wD", "old_status": "In Progress \ud83c\udfd7\ufe0f", "new_status": "In development"}
{"number": 1229, "id": "I_kwhjkol6Is5aw2yu", "old_status": "In Progress \ud83c\udfd7\ufe0f", "new_status": "In development"}
In order to use the GitHub API to add the issues to the new project, we'll need its ID. We can get this easily using the GitHub CLI:
project_id=$(gh api graphql --jq '.data.organization.projectV2.id' -f query='
query{
organization(login:"form3tech"){
projectV2(number:345){
id
}
}
}')
Project v2 boards have a more flexible status configuration than the columns in a classic board, so in order to make use of them via the API you need to know:
We can find all this out in one go with a single request to the GitHub API:
gh api graphql -f query='
query{
organization(login: "form3tech"){
projectV2(number: 345) {
field(name:"Status"){
__typename
... on ProjectV2SingleSelectField{
id
options{
id
name
}
}
}
}
}
}' | jq
The output is something like this:
{
"data": {
"organization": {
"projectV2": {
"field": {
"__typename": "ProjectV2SingleSelectField",
"id": "PVTSSF_lhjkerhkjhjkJlw9zgF9whc",
"options": [
{
"id": "ehjkhkj6",
"name": "Product planning"
},
{
"id": "hjkhkje4",
"name": "Ready to develop"
},
{
"id": "hukjhkjh",
"name": "In development"
},
{
"id": "dhjkhk31",
"name": "Ready for demo"
},
{
"id": "4hjkhkcb",
"name": "Blocked"
},
{
"id": "hjkh6657",
"name": "Approved"
}
]
}
}
}
}
}
From this output, we can find the status field ID (jq '.data.organization.projectV2.field.id' | sed 's/"//g'), and the status options (jq '.data.organization.projectV2.field.options')
Now, we can bring all of this data together in a Bash script which iterates over each of the issue lines outputted by our Python script, and adds the issue to the correct status on the new board:
This script assumes that:
set -eu -o pipefail
echo -n "$issue_statuses" | while read -r issue; do
# Parse each JSON object into the variables we need.
issue_id=$(echo -n "$issue" | jq '.id' | sed 's/"//g')
issue_number=$(echo -n "$issue" | jq '.number')
new_status=$(echo -n "$issue" | jq '.new_status' | sed 's/"//g')
status_option_id=$(echo -n "$status_option_ids" | jq ".[] | select(.name == \"$new_status\").id" | sed 's/"//g')
# Print out the variables we're using for the user to see.
echo "issue id: $issue_id"
echo "issue number: $issue_number"
echo "new status: $new_status"
echo "status option id: $status_option_id"
# Check that we've got valid data.
if [[ -z "$issue_id" || -z "$issue_number" || -z "$new_status" || -z "$status_option_id" ]]; then
echo "invalid args"
exit 1
fi
echo "adding issue number $issue_number to new board"
# First, add the issue to the new project, and store the item/card ID in a variable.
item_id=$(gh api graphql -f query='
mutation{
addProjectV2ItemById(input:{
contentId:"'"$issue_id"'",
projectId:"'"$project_id"'"
}){
item{
id
project{
title
}
}
}
}' | jq '.data.addProjectV2ItemById.item.id' | sed 's/"//g')
echo "moving issue number $issue_number to new status $new_status"
# Then, assign the correct status to the new project item/card.
gh api graphql --jq '.data.updateProjectV2ItemFieldValue.projectV2Item.fieldValueByName.name' -f query='
mutation{
updateProjectV2ItemFieldValue(input:{
itemId:"'"$item_id"'",
value:{singleSelectOptionId:"'"$status_option_id"'"},
fieldId:"'"$status_field_id"'",
projectId:"'"$project_id"'",
clientMutationId:"status-update"
}){
projectV2Item{
fieldValueByName(name: "Status"){
__typename
... on ProjectV2ItemFieldSingleSelectValue{
name
}
}
}
}
}'
done
So there it is: it's a bit painful, but not possible via the GitHub UI. You need to make a few requests to the GraphQL to get the data you need, and re-shape some of the output into a format that's usable. For small project boards, you could probably do this manually, but if you need to migrate hundreds of issues then I hope you find this post useful!
Written by
Andy Kuszyk is a Staff Engineer at Form3, based in Southampton. He's been working as a software engineer for 8 years with a variety of technologies, including .NET, Python and most recently Go. Check out more of his tech articles on his blog.
Blogs · 10 min
A subdomain takeover is a class of attack in which an adversary is able to serve unauthorized content from victim's domain name. It can be used for phishing, supply chain compromise, and other forms of attacks which rely on deception. You might've heard about CNAME based or NS based subdomain takeovers.
October 27, 2023
Blogs · 4 min
In this blogpost, David introduces us to the five W's of information gathering - Who? What? When? Where? Why? Answering the five Ws helps Incident Managers get a deeper understanding of the cause and impact of incidents, not just their remedy, leading to more robust solutions. Fixing the cause of an outage is only just the beginning and the five Ws pave the way for team collaboration during investigations.
July 26, 2023
Blogs · 4 min
Patrycja, Artur and Marcin are engineers at Form3 and some of our most accomplished speakers. They join us to discuss their motivations for taking up the challenge of becoming conference speakers, tell us how to find events to speak at and share their best advice for preparing engaging talks. They offer advice for new and experienced speakers alike.
July 19, 2023