🧑💻 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/jsonStructuring 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
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}/eventsusing http.Get()URL construction: Uses
fmt.Sprintf()to dynamically insert the username into the GitHub API endpointError 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 1if 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 flexibilityJSON unmarshaling: Uses
json.Unmarshal()to parse the byte data into the events structureParse error handling: Checks for JSON parsing errors and exits with status
code 1if 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{}, eacheventis a map with keys like"id","type","repo","payload", etc.The
for _, event := range eventsloop 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 extractrepo["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
sand 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. 🙌🎊



