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 · 4 min
Kaspar Von Grünberg is a the CEO and founder of Humanitec. He joins us to discuss what an Internal Developer Platform is and what to focus on when you're building your own. Finally, he provides an overview of Humanitec's platform, which provides open-source tools you can use when you're building your own IDP.
March 15, 2023
Blogs · 3 min
No system is free of security vulnerabilities, which can be exploited to gain access to restricted resources. Ethical hackers are our allies, using the same techniques as malicious actors to help us find and fix the security vulnerabilities of our systems. In this short introductory article, we explore: the different types of hackers, the goals of ethical hacking and the main activities of ethical hackers.
March 2, 2023
Blogs · 4 min
Alexandra Forsberg is a Talent Acquisition Lead at Form3. She joins us to share tips for landing your next remote job. Alexandra covers all aspects of the interviewing process including where to find remote opportunities, how to stand out to hiring managers and how to prepare for a remote interview. Finally, she shares Form3's approach to the interview process.
February 15, 2023