Skip to main content

Command Palette

Search for a command to run...

🧑‍💻 Track Your GitHub Activity from the Terminal — Build a CLI in Go!

Published
9 min read
🧑‍💻 Track Your GitHub Activity from the Terminal — Build a CLI in Go!

After building a Task Tracker CLI and a Movie CLI fetching data from TMDB,
I wanted to explore something a bit more developer-centric — a CLI that interacts with GitHub’s public API.

The idea

Build a small Go CLI app that takes a GitHub username and displays their recent activity right in the terminal 💻

To learn about the problem statement , read : https://roadmap.sh/projects/github-user-activity

🚀 Project Overview

We’ll build a command-line tool that:

  • Accepts a GitHub username as input

  • Fetches the user’s latest events from the GitHub API

  • Displays them in a readable format — push events, PRs, stars, etc.

By the end, you’ll learn:

  • Making HTTP GET requests in Go

  • Handling JSON data using encoding/json

  • Structuring Go CLI apps with Cobra

Project URL : https://github.com/pranav767/GO_PROJECTS/tree/main/github-activity

Project Structure

What is an API?

An API (Application Programming Interface) acts as a bridge between two systems or applications.
It defines how software components should interact — basically, it’s a set of rules that let one program talk to another.

For example, when Go CLI fetches movie data or GitHub activity, it’s calling the respective service’s API to get structured information back — usually in JSON format.

How is an API different from HTTP request?

An HTTP request is just the method of communication — it’s how your program sends or receives data over the web (using methods like GET, POST, etc.).

An API, on the other hand, is the contract that defines what kind of data you can send or receive, and in what format.

So when you hit https://api.github.com/users/<username>/events,

  • the HTTP request is the action (GET)

  • the API defines what data you’ll get back and how it’s structured.

Using Github API to get user activity

Project URL : https://github.com/pranav767/GO_PROJECTS/tree/main/github-activity
Project Idea: https://roadmap.sh/projects/github-user-activity

To get started do

go mod init github-user-activity
cobra-cli init github-user-activity

This will create the basic structure of our project, it will have a cmd folder which will have root.go file this will be a entry point for the cli We dont need to add a sub command/command, we just have to make sure there is always a single argumnent (which will be username) We can do that by Adding following line in &cobra.command

Args: cobra.ExactArgs(1),

We’ll have to fetch this input while executing the api, Lets have a look at github event api

https://docs.github.com/en/rest/activity/events?apiVersion=2022-11-28#list-events-for-the-authenticated-user

curl -L -H "Accept: application/vnd.github+json"-H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/users/USERNAME/events

We can test this out in terminal for now :

[
  {
    "id": "53464046510",
    "type": "PushEvent",
    "actor": {
      "id": 116432455,
      "login": "pranav767",
      "display_login": "pranav767",
      "gravatar_id": "",
      "url": "https://api.github.com/users/pranav767",
      "avatar_url": "https://avatars.githubusercontent.com/u/116432455?"
    },
    "repo": {
      "id": 1010575942,
      "name": "pranav767/GO_PROJECTS",
      "url": "https://api.github.com/repos/pranav767/GO_PROJECTS"
    },
    "payload": {
      "repository_id": 1010575942,
      "push_id": 26191155155,
      "size": 1,
      "distinct_size": 1,
      "ref": "refs/heads/main",
      "head": "85d6ec21c9eef2a45a3ffc8d3d6280184f339bcb",
      "before": "e7d4c456e6027a844ab0e69ad77e98a54f92d4a4",
      "commits": [
        {
          "sha": "85d6ec21c9eef2a45a3ffc8d3d6280184f339bcb",
          "author": {
            "email": "itsmepranav888@gmail.com",
            "name": "Pranav Patil"
          },
          "message": "Update README.md",
          "distinct": true,
          "url": "https://api.github.com/repos/pranav767/GO_PROJECTS/commits/85d6ec21c9eef2a45a3ffc8d3d6280184f339bcb"
        }
      ]
    },
    "public": true,
    "created_at": "2025-08-17T19:58:51Z"
  },
  {
    "id": "53464031090",
    "type": "PushEvent",
    "actor": {
      "id": 116432455,
      "login": "pranav767",
      "display_login": "pranav767",
      "gravatar_id": "",
      "url": "https://api.github.com/users/pranav767",
      "avatar_url": "https://avatars.githubusercontent.com/u/116432455?"
    },
    "repo": {
      "id": 1010575942,
      "name": "pranav767/GO_PROJECTS",
      "url": "https://api.github.com/repos/pranav767/GO_PROJECTS"
    },
    "payload": {
      "repository_id": 1010575942,
      "push_id": 26191145262,
      "size": 1,
      "distinct_size": 1,
      "ref": "refs/heads/feature/add-task-tracker",
      "head": "1b14db0e014d504cbd3494d69d5b95fe5a5cad89",
      "before": "9dfa1ef8a0c6f7cc925b092b0977ac9ac059c824",
      "commits": [
        {
          "sha": "1b14db0e014d504cbd3494d69d5b95fe5a5cad89",
          "author": {
            "email": "pranavppatil767@gmail.com",
            "name": "Pranav"
          },
          "message": "update readme",
          "distinct": true,
          "url": "https://api.github.com/repos/pranav767/GO_PROJECTS/commits/1b14db0e014d504cbd3494d69d5b95fe5a5cad89"
        }
      ]
    },
    "public": true,
    "created_at": "2025-08-17T19:57:33Z"
  },
  {
    "id": "53463995847",
    "type": "PushEvent",
    "actor": {
      "id": 116432455,
      "login": "pranav767",
      "display_login": "pranav767",
      "gravatar_id": "",
      "url": "https://api.github.com/users/pranav767",
      "avatar_url": "https://avatars.githubusercontent.com/u/116432455?"
    },
    "repo": {
      "id": 1010575942,
      "name": "pranav767/GO_PROJECTS",
      "url": "https://api.github.com/repos/pranav767/GO_PROJECTS"
    },
    "payload": {
      "repository_id": 1010575942,
      "push_id": 26191122317,
      "size": 3,
      "distinct_size": 1,
      "ref": "refs/heads/main",
      "head": "e7d4c456e6027a844ab0e69ad77e98a54f92d4a4",
      "before": "5c189ada78f847dcec62b309c6946da026a5f315",
      "commits": [
        {
          "sha": "16bff94c98b9bfd9d19c3225914b3991035bbc52",
          "author": {
            "email": "pranavppatil767@gmail.com",
            "name": "Pranav"
          },
          "message": "add task tracker app",
          "distinct": false,
          "url": "https://api.github.com/repos/pranav767/GO_PROJECTS/commits/16bff94c98b9bfd9d19c3225914b3991035bbc52"
        },
        {
          "sha": "9dfa1ef8a0c6f7cc925b092b0977ac9ac059c824",
          "author": {
            "email": "pranavppatil767@gmail.com",
            "name": "Pranav"
          },
          "message": "remove LICENSE",
          "distinct": false,
          "url": "https://api.github.com/repos/pranav767/GO_PROJECTS/commits/9dfa1ef8a0c6f7cc925b092b0977ac9ac059c824"
        },
        {
          "sha": "e7d4c456e6027a844ab0e69ad77e98a54f92d4a4",
          "author": {
            "email": "itsmepranav888@gmail.com",
            "name": "Pranav Patil"
          },
          "message": "Merge pull request #9 from pranav767/feature/add-task-tracker\n\nadd task tracker app",
          "distinct": true,
          "url": "https://api.github.com/repos/pranav767/GO_PROJECTS/commits/e7d4c456e6027a844ab0e69ad77e98a54f92d4a4"
        }
      ]
    },
    "public": true,
    "created_at": "2025-08-17T19:54:36Z"
  },

Works just fine.

Run: func(cmd *cobra.Command, args []string) { username := args[0] events.FetchEvents(username) },

From above code we have just passed the username parameter to a function call FetchEvents, keeping this simple we will write the function definition to another folder called events/events.go

package events

import (
    "fmt"
    "net/http"
    "os"
    "io/ioutil"
    "encoding/json"
)

// Function to fetch events for a given username
func FetchEvents(username string) {

    resp, err := http.Get(fmt.Sprintf("https://api.github.com/users/%s/events", username))
    if err != nil {
        fmt.Println("Error fetching URL:", err)
        os.Exit(1)
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error reading response body:", err)
        os.Exit(1)
    }

    var events []map[string]interface{}
    err = json.Unmarshal(body, &events)
    if err != nil {
        fmt.Println("Error unmarshalling JSON:", err)
        os.Exit(1)
    }

    // Gives a sample output of the first event
    //b, _ := json.MarshalIndent(events[2], "", "  ")
    //fmt.Println(string(b))

    for _, event := range events {
        eventType, _ := event["type"].(string)
        repo := ""
        if r, ok := event["repo"].(map[string]interface{}); ok {
            repo, _ = r["name"].(string)
        } else if r, ok := event["repo"].(map[string]string); ok {
            repo = r["name"]
        } else if r, ok := event["repo"].(map[string]any); ok {
            if name, ok := r["name"].(string); ok {
                repo = name
            }
        }
        switch eventType {
        case "PushEvent":
            payload, _ := event["payload"].(map[string]interface{})
            commits := 0
            if c, ok := payload["commits"].([]interface{}); ok {
                commits = len(c)
            }
            fmt.Printf("Pushed %d commits to %s\n", commits, repo)
        case "IssuesEvent":
            payload, _ := event["payload"].(map[string]interface{})
            action, _ := payload["action"].(string)
            fmt.Printf("%s an issue in %s\n", capitalize(action), repo)
        case "WatchEvent":
            fmt.Printf("Starred %s\n", repo)
        case "ForkEvent":
            fmt.Printf("Forked %s\n", repo)
        case "PullRequestEvent":
            payload, _ := event["payload"].(map[string]interface{})
            action, _ := payload["action"].(string)
            fmt.Printf("%s a pull request in %s\n", capitalize(action), repo)
        // Add more cases as needed for other event types
        default:
            // Uncomment to see all event types
            // fmt.Printf("%s in %s\n", eventType, repo)
        }
    }
}

func capitalize(s string) string {
    if len(s) == 0 {
        return s
    }
    return string(s[0]-32) + s[1:]
}

Let us try to understand the code:

Package & Imports

package events

import (
    "fmt"
    "net/http"
    "os"
    "io/ioutil"
    "encoding/json"
)

package events , tells us that this file belongs to a package events,
Next we import few libraries required, it uses fmt for formatted output, net/http for HTTP requests, os for system operations, io/ioutil for reading response data, and encoding/json for JSON processing

Function Signature

  • Function name: FetchEvents takes a single username parameter of type string

  • Purpose: Retrieves and displays recent GitHub activity for the specified user

func FetchEvents(username string) {

HTTP request Handling

  • API call: Makes a GET request to https://api.github.com/users/{username}/events using http.Get()

  • URL construction: Uses fmt.Sprintf() to dynamically insert the username into the GitHub API endpoint

  • Error handling: Immediately checks for HTTP request errors and exits the program with status code 1 if the request fails

  • Resource management: Uses defer resp.Body.Close() to ensure the HTTP response body is properly closed when the function exits

resp, err := http.Get(fmt.Sprintf("https://api.github.com/users/%s/events", username))
if err != nil {
    fmt.Println("Error fetching URL:", err)
    os.Exit(1)
}
defer resp.Body.Close()

Response Processing

  • Body reading: Uses ioutil.ReadAll() to read the entire HTTP response body into a byte slice

  • Error checking: Includes error handling for potential issues reading the response data

  • Program termination: Exits with error code 1 if reading the response body fails

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
    fmt.Println("Error reading response body:", err)
    os.Exit(1)
}

JSON data parsing

  • Data structure: Declares events as a slice of maps with string keys and interface{} values for flexibility

  • JSON unmarshaling: Uses json.Unmarshal() to parse the byte data into the events structure

  • Parse error handling: Checks for JSON parsing errors and exits with status code 1 if unmarshaling fails

var events []map[string]interface{}
err = json.Unmarshal(body, &events)
if err != nil {
    fmt.Println("Error unmarshalling JSON:", err)
    os.Exit(1)
}

Filtering response

If you look at the curl response , it will look like:

[
  {
    "id": "53815473799",
    "type": "DeleteEvent",
    ...
  },
  {
    "id": "53815473368",
    "type": "PushEvent",
  },
  {
    "id": "53815473055",
    "type": "PullRequestEvent",
   },
]
  • The GitHub API returns an array ([]) of event objects in JSON.

  • After unmarshalling into events []map[string]interface{}, each event is a map with keys like "id", "type", "repo", "payload", etc.

  • The for _, event := range events loop goes through every event in that array.

for _, event := range events {
    eventType, _ := event["type"].(string)
    repo := ""
    if r, ok := event["repo"].(map[string]interface{}); ok {
        repo, _ = r["name"].(string)
    } else if r, ok := event["repo"].(map[string]string); ok {
        repo = r["name"]
    } else if r, ok := event["repo"].(map[string]any); ok {
        if name, ok := r["name"].(string); ok {
            repo = name
        }
    }

Extracting Event type & repo name

  • eventType, _ := event["type"].(string)

  • Tries to read the "type" field (e.g., "PushEvent", "IssuesEvent", "ForkEvent") as a string.

  • This helps decide what kind of activity the user performed.

  • GitHub events usually contain a "repo" object → {"name": "username/repo"}.

  • The code does type assertions (map[string]interface{}, map[string]string, map[string]any) to safely extract repo["name"].

  • This ensures compatibility if the JSON format varies slightly.

Using switch case to handle event types

switch eventType {
case "PushEvent":
    payload, _ := event["payload"].(map[string]interface{})
    commits := 0
    if c, ok := payload["commits"].([]interface{}); ok {
        commits = len(c)
    }
    fmt.Printf("Pushed %d commits to %s\n", commits, repo)
case "IssuesEvent":
    payload, _ := event["payload"].(map[string]interface{})
    action, _ := payload["action"].(string)
    fmt.Printf("%s an issue in %s\n", capitalize(action), repo)
case "WatchEvent":
    fmt.Printf("Starred %s\n", repo)
case "ForkEvent":
    fmt.Printf("Forked %s\n", repo)
case "PullRequestEvent":
    payload, _ := event["payload"].(map[string]interface{})
    action, _ := payload["action"].(string)
    fmt.Printf("%s a pull request in %s\n", capitalize(action), repo)
    // Add more cases as needed for other event types
default:
    // Uncomment to see all event types
    // fmt.Printf("%s in %s\n", eventType, repo)
    }
}

Depending on eventType, different cases are executed:

  • PushEvent → Reads payload["commits"], counts commits, prints message like "Pushed 3 commits to repoX"

  • IssuesEvent → Reads payload["action"] (e.g., "opened", "closed") and prints "Opened an issue in repoX"

  • WatchEvent → Prints "Starred repoX"

  • ForkEvent → Prints "Forked repoX"

  • PullRequestEvent → Reads payload["action"] and prints "Opened a pull request in repoX"

  • Default → (currently commented) would print other event types not explicitly handled.

Function Capitalize

func capitalize(s string) string {
    if len(s) == 0 {
        return s
    }
    return string(s[0]-32) + s[1:]
}
  • The function takes a string s and ensures that its first character is uppercase.

  • For example: "opened""Opened".

Executing the main file

Flow for the program goes like, a user executes command go run main.go <username>, starting point will be main.go file, which will execute root.go,this file will ensure we have one argument which will be username and a function in events.go fetchEvents will be called this will list all the events for the given user.

jinx@MSI:~/go/GO_PROJECTS/github-activity$ go run main.go pranav767
Pushed 1 commits to pranav767/GO_PROJECTS
Pushed 1 commits to pranav767/GO_PROJECTS
Pushed 3 commits to pranav767/GO_PROJECTS
Closed a pull request in pranav767/GO_PROJECTS
Opened a pull request in pranav767/GO_PROJECTS
Pushed 1 commits to pranav767/GO_PROJECTS
Pushed 2 commits to pranav767/GO_PROJECTS
Closed a pull request in pranav767/GO_PROJECTS
Opened a pull request in pranav767/GO_PROJECTS
Pushed 1 commits to pranav767/GO_PROJECTS
Pushed 7 commits to pranav767/GO_PROJECTS
Closed a pull request in pranav767/GO_PROJECTS
Pushed 1 commits to pranav767/GO_PROJECTS
Pushed 1 commits to pranav767/GO_PROJECTS
Pushed 1 commits to pranav767/GO_PROJECTS
Pushed 1 commits to pranav767/GO_PROJECTS
Pushed 1 commits to pranav767/GO_PROJECTS
Opened a pull request in pranav767/GO_PROJECTS
Pushed 2 commits to pranav767/GO_PROJECTS
Closed a pull request in pranav767/GO_PROJECTS
Opened a pull request in pranav767/GO_PROJECTS

Works just fine. 🙌🎊

More from this blog

Pranav's Blog

27 posts