SaltyCrane Blog — Notes on JavaScript and web development

How to download pull request metadata using the GitHub GraphQL API

This is a TypeScript Node.js script to download GitHub pull request information (title, body, comments, etc.) using the GitHub GraphQL API. The data is saved in a JSON file.

The GitHub repo is here: download-github-prs.

Create a GitHub personal access token

Create a GitHub personal access token as described here (no checkboxes need to be selected for public repos): https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token This is used to access the GitHub GraphQL API.

Intall libraries

  • Create a directory and cd into it
    $ mkdir -p /tmp/my-prs
    $ cd /tmp/my-prs 
    
  • Create package.json file:
    {
      "scripts": {
        "download": "ts-node index.ts"
      },
      "dependencies": {
        "node-fetch": "^2.6.1"
      },
      "devDependencies": {
        "@types/node-fetch": "^2.5.7",
        "ts-node": "^9.0.0",
        "typescript": "^4.1.2"
      }
    }
    
  • Install
    $ npm install 
    
  • Create script

    Creat a file, /tmp/my-prs/index.ts, replacing XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX with the GitHub personal access token described above.

    import * as fs from "fs";
    import fetch from "node-fetch";
    
    const GITHUB_TOKEN = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
    const GITHUB_GRAPHQL_API_URL = "https://api.github.com/graphql";
    const OUTPUT_DIR = "/tmp/my-prs";
    const REPO_OWNER = "facebookexperimental";
    const REPO_NAME = "Recoil";
    
    fetchPullRequest(1);
    
    /**
     * fetchPullRequest
     */
    async function fetchPullRequest(prNumber: number) {
      const reactionFragment = `
        content
        user {
          login
        }
      `;
      const userContentEditFragment = `
        createdAt
        deletedAt
        deletedBy {
          login
        }
        diff
        editedAt
        editor {
          login
        }
        updatedAt
      `;
      const commentFragment = `
        author {
          login
        }
        body
        createdAt
        reactions(first: 100) {
          nodes {
            ${reactionFragment}
          }
        }
        userContentEdits(first: 100) {
          nodes {
            ${userContentEditFragment}
          }
        }
      `;
      const query = `
      query {
        repository(owner: "${REPO_OWNER}", name: "${REPO_NAME}") {
          nameWithOwner
          pullRequest(number: ${prNumber}) {
            author { login }
            baseRefName
            baseRefOid
            body
            closedAt
            comments(first: 100) {
              nodes {
                ${commentFragment}
              }
            }
            commits(first: 250) {
              nodes {
                commit {
                  oid
                }
              }
            }
            createdAt
            files(first: 100) {
              nodes { path }
            }
            headRefName
            headRefOid
            mergeCommit { oid }
            merged
            mergedAt
            mergedBy { login }
            number
            publishedAt
            reactions(first: 10) {
              nodes {
                ${reactionFragment}
              }
            }
            reviews(first: 10) {
              nodes {
                author { login }
                body
                comments(first: 10) {
                  nodes {
                    ${commentFragment}
                  }
                }
                commit {
                  oid
                }
                createdAt
                editor { login }
                publishedAt
                reactions(first: 10) {
                  nodes {
                    ${reactionFragment}
                  }
                }
                resourcePath
                submittedAt
                updatedAt
                userContentEdits(first: 10) {
                  nodes {
                    ${userContentEditFragment}
                  }
                }
              }
            }
            state
            title
            updatedAt
            userContentEdits(first: 10) {
              nodes {
                ${userContentEditFragment}
              }
            }
          }
        }
      }
      `;
    
      // make graphql query and strigify the response
      const resp = await fetchQuery(query);
      const respStr = JSON.stringify(resp, null, 2);
    
      // save json file
      const filepath = [
        `${OUTPUT_DIR}/`,
        `${REPO_NAME}-pr-${String(prNumber).padStart(4, "0")}.json`,
      ].join("");
      console.log(`Saving ${filepath}...`);
      fs.writeFileSync(filepath, respStr);
    }
    
    /**
     * fetchQuery
     */
    function fetchQuery(query: string, variables: Record<string, any> = {}) {
      return fetch(GITHUB_GRAPHQL_API_URL, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `bearer ${GITHUB_TOKEN}`,
        },
        body: JSON.stringify({
          query,
          variables,
        }),
      }).then((response) => {
        return response.json();
      });
    }
    

    Run it

    $ npm run download 
    

    Output

    This produces the following JSON file

    $ cat /tmp/my-prs/Recoil-pr-0001.json
    {
      "data": {
        "repository": {
          "nameWithOwner": "facebookexperimental/Recoil",
          "pullRequest": {
            "author": {
              "login": "facebook-github-bot"
            },
            "baseRefName": "master",
            "baseRefOid": "40e870caadc159a87e81be291ff641410ab32e8f",
            "body": "This is pull request was created automatically because we noticed your project was missing a Contributing file.\n\nCONTRIBUTING files explain how a developer can contribute to the project - which you should actively encourage.\n\nThis PR was crafted with love by Facebook's Open Source Team.",
            "closedAt": "2020-05-13T04:12:15Z",
            "comments": {
              "nodes": [
                {
                  "author": {
                    "login": "davidmccabe"
                  },
                  "body": "Already added this manually.",
                  "createdAt": "2020-05-13T04:12:15Z",
                  "reactions": {
                    "nodes": []
                  },
                  "userContentEdits": {
                    "nodes": []
                  }
                }
              ]
            },
            "commits": {
              "nodes": [
                {
                  "commit": {
                    "oid": "96f91679540362fa96a6c92611a8ef5621447b42"
                  }
                }
              ]
            },
            "createdAt": "2020-05-06T22:31:01Z",
            "files": {
              "nodes": [
                {
                  "path": "CONTRIBUTING.md"
                }
              ]
            },
            "headRefName": "automated_fixup_contributing_file_exists",
            "headRefOid": "96f91679540362fa96a6c92611a8ef5621447b42",
            "mergeCommit": null,
            "merged": false,
            "mergedAt": null,
            "mergedBy": null,
            "number": 1,
            "publishedAt": "2020-05-06T22:31:01Z",
            "reactions": {
              "nodes": []
            },
            "reviews": {
              "nodes": []
            },
            "state": "CLOSED",
            "title": "Adding Contributing file",
            "updatedAt": "2020-10-07T20:23:05Z",
            "userContentEdits": {
              "nodes": []
            }
          }
        }
      }
    }
    

Comments