Migrating to GitHub "Issue Types"

In January GitHub announced a new way to classify issues by "issue types". Unlike labels, issue types are managed on an organization level and each issue can only have one type. By default, GitHub creates 3 issue types: "Feature", "Bug" and "Task".

If you used labels for this purpose before, you might want to migrate your labels such as "enhancement" and "bug" to issue types. You can automate this process using this Node.js script. Afterwards, your legacy labels can be safely deleted.

const GH_TOKEN = "...";
const GH_ORG = "...";
const GH_REPO = "...";
// Replace "enhancement" label with "Feature" issue type
const LABEL_TO_REMOVE = "enhancement";
const ISSUE_TYPE_TO_SET = "Feature";

const headers = {
  Authorization: `bearer ${GH_TOKEN}`,
  Accept: "application/vnd.github.v3+json",
};
const baseUrl = `https://api.github.com/repos/${GH_ORG}/${GH_REPO}`;

while (true) {
  // Fetch a page of issues, including closed ones
  const response = await fetch(`${baseUrl}/issues?labels=${LABEL_TO_REMOVE}&state=all`, {headers});
  if (response.status !== 200) {
    throw new Error(`failed to fetch issues: ${response.status}`);
  }

  const issues = await response.json();
  if (issues.length === 0) {
    break;
  }

  for (const issue of issues) {
    console.log(`processing issue ${issue["number"]}`);
    // Set issue type if not set
    if (issue["type"]?.["name"] !== ISSUE_TYPE_TO_SET) {
      console.log(`setting issue type`);
      const issueTypeUpdateResponse = await fetch(`${baseUrl}/issues/${issue["number"]}`, {
        method: "PATCH",
        body: JSON.stringify({type: ISSUE_TYPE_TO_SET}),
        headers,
      });
      if (issueTypeUpdateResponse.status !== 200) {
        throw new Error(`unexpected status code: ${issueTypeUpdateResponse.status}`);
      }
    }

    console.log(`deleting label`);
    const labelDeleteResponse = await fetch(`${baseUrl}/issues/${issue["number"]}/labels/${LABEL_TO_REMOVE}`, {
      method: "DELETE",
      headers,
    });
    if (labelDeleteResponse.status !== 200) {
      throw new Error(`unexpected status code: ${labelDeleteResponse.status}`);
    }
  }
}