{"schemaVersion":"1.0","type":"Article","types":["Article"],"slug":"building-a-github-contribution-city-transforming-developer-history-into-a-3d-skyline-rlzdn","url":"https://zyvop.com/building-a-github-contribution-city-transforming-developer-history-into-a-3d-skyline-rlzdn","title":"Building a GitHub Contribution City: Transforming Developer History into a 3D Skyline","subtitle":null,"tldr":"Build your contribution city! Developer profiles are our digital resumes, but let's be honest: the...","keywords":["githubactions","Software","GitHub","cicd","ibmbob"],"entities":["Alain Airom (Ayrom)","Build Engineer","githubactions","Software","GitHub","cicd","ibmbob","ZyVOP"],"keyTakeaways":["Build your contribution city!","Developer profiles are our digital resumes, but let's be honest: the standard 53-week GitHub contribution heatmap grid is starting to feel a bit flat.","In this post, we’ll explore GitHub Contribution City, a production-grade application built in Go that transforms any GitHub user's contribution history into a stunning 2D SVG skyline, a 3D isometric SVG, and an interactive WebGL 3D world powered by Three.js."],"headings":["Introduction &amp; Project Inspiration","Detailed Architecture and Implementation","System Architecture","Configuration Management (pkg/config)","Fetching Contribution Calendars (pkg/github)","Logarithmic City Layout Algorithm (pkg/city)","Main Application Entrypoint (cmd/city/main.go)","Automation with GitHub Actions","Conclusion","Links"],"outboundLinks":["https://dzone.com/articles/build-a-github-contribution-city-with-java-and-qua","https://github.com/aairom/github-contribution-city","https://github.com/myfear/quarkus-contribution-city"],"contentText":"Build your contribution city! Developer profiles are our digital resumes, but let's be honest: the standard 53-week GitHub contribution heatmap grid is starting to feel a bit flat. What if instead of looking at flat green squares, we could walk through a bustling cyberpunk metropolis, gaze up at towering skyscrapers built from our busiest commit weeks, and watch our coding history come alive in 3D? In this post, we’ll explore GitHub Contribution City, a production-grade application built in Go that transforms any GitHub user's contribution history into a stunning 2D SVG skyline, a 3D isometric SVG, and an interactive WebGL 3D world powered by Three.js. Before moving further, I want to clarify that the inspiration for this project comes from a blog post by Markus Eisele—a highly esteemed IBM colleague—which you can read here. Reading it gave me the idea to build this using IBM Bob. However, Markus is a true Java enthusiast who isn't particularly fond of languages like Python or JavaScript. Personally, I'm not a fan of Java :) so I asked Bob to adapt the codebase into Go instead. We all have our own brand of madness! 😵‍💫 Introduction &amp; Project Inspiration The initial spark for this project was inspired by Markus Eisele’s fantastic Quarkus Contribution City concept. While the original implementation was written in Java, we wanted to build a modern, lightning-fast, zero-dependency binary in Go that can run locally as a CLI tool, deploy as a high-performance web server, or run automatically via GitHub Actions. Whether you want to generate a gorgeous SVG badge for your README.md, view your personal tech metropolis in 3D, or spin up an automated daily action that tracks your team's contributions, GitHub Contribution City turns your code commits into architectural marvels. Detailed Architecture and Implementation System Architecture The application is structured around a clean modular pipeline: input configurations and remote API data feed into our core scene builder, which in turn delegates to specialized renderers. Configuration Management (pkg/config) The configuration manager handles environment variables, .env configuration files, and sensible defaults (such as defaulting to torvalds if no user is specified). package config import ( \"bufio\" \"os\" \"strconv\" \"strings\") // Config represents runtime configuration for GitHub Contribution Citytype Config struct { GitHubToken string `json:\"github_token\"` GitHubUsername string `json:\"github_username\"` Weeks int `json:\"weeks\"` MaxHeight int `json:\"max_height\"` Theme string `json:\"theme\"` Port int `json:\"port\"` Host string `json:\"host\"` OutputDir string `json:\"output_dir\"` InputDir string `json:\"input_dir\"`} // DefaultConfig returns default sensible parametersfunc DefaultConfig() *Config { return &amp;Config{ GitHubToken: \"\", GitHubUsername: \"torvalds\", Weeks: 53, MaxHeight: 14, Theme: \"github-dark\", Port: 8088, Host: \"0.0.0.0\", OutputDir: \"output\", InputDir: \"input\", }} // LoadConfig loads configuration from optional .env file and environment variablesfunc LoadConfig(envFile string) *Config { cfg := DefaultConfig() if envFile != \"\" { _ = loadDotEnv(envFile) } else if _, err := os.Stat(\".env\"); err == nil { _ = loadDotEnv(\".env\") } if val := os.Getenv(\"GITHUB_TOKEN\"); val != \"\" { cfg.GitHubToken = strings.TrimSpace(val) } if val := os.Getenv(\"GITHUB_USERNAME\"); val != \"\" { cfg.GitHubUsername = strings.TrimSpace(val) } if val := os.Getenv(\"CITY_WEEKS\"); val != \"\" { if w, err := strconv.Atoi(val); err == nil &amp;&amp; w &gt;= 1 &amp;&amp; w &lt;= 53 { cfg.Weeks = w } } if val := os.Getenv(\"CITY_MAX_HEIGHT\"); val != \"\" { if h, err := strconv.Atoi(val); err == nil &amp;&amp; h &gt;= 2 &amp;&amp; h &lt;= 50 { cfg.MaxHeight = h } } if val := os.Getenv(\"CITY_THEME\"); val != \"\" { cfg.Theme = strings.TrimSpace(val) } if val := os.Getenv(\"PORT\"); val != \"\" { if p, err := strconv.Atoi(val); err == nil &amp;&amp; p &gt; 0 { cfg.Port = p } } return cfg} func loadDotEnv(filepath string) error { file, err := os.Open(filepath) if err != nil { return err } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == \"\" || strings.HasPrefix(line, \"#\") { continue } parts := strings.SplitN(line, \"=\", 2) if len(parts) == 2 { key := strings.TrimSpace(parts[0]) val := strings.TrimSpace(parts[1]) val = strings.Trim(val, `\"'`) if os.Getenv(key) == \"\" { _ = os.Setenv(key, val) } } } return scanner.Err()}Enter fullscreen mode Exit fullscreen mode Fetching Contribution Calendars (pkg/github) The GitHub client seamlessly switches between authenticated GraphQL API queries (supporting private and internal commits if a PAT is provided) and a robust HTML scraper fallback for public profiles. package github import ( \"bytes\" \"context\" \"encoding/json\" \"fmt\" \"io\" \"net/http\" \"time\") // ContributionDay represents one day's contributionstype ContributionDay struct { ContributionCount int `json:\"contributionCount\"` ContributionLevel string `json:\"contributionLevel\"` Date string `json:\"date\"` Weekday int `json:\"weekday\"` ParsedDate time.Time `json:\"-\"`} // ContributionWeek represents a week of days (up to 7 days)type ContributionWeek struct { ContributionDays []ContributionDay `json:\"contributionDays\"` FirstDay string `json:\"firstDay\"`} // ContributionCalendar is the complete calendar returned by GitHubtype ContributionCalendar struct { TotalContributions int `json:\"totalContributions\"` Weeks []ContributionWeek `json:\"weeks\"` Colors []string `json:\"colors\"` Username string `json:\"username\"`} // Client is a GitHub API client that can fetch contribution calendarstype Client struct { token string httpClient *http.Client graphQLURL string} func NewClient(token string) *Client { return &amp;Client{ token: token, httpClient: &amp;http.Client{ Timeout: 15 * time.Second, }, graphQLURL: \"https://api.github.com/graphql\", }} // FetchCalendar fetches contribution data using GraphQL or public scraping fallbackfunc (c *Client) FetchCalendar(ctx context.Context, username string) (*ContributionCalendar, error) { if c.token != \"\" { cal, err := c.fetchGraphQL(ctx, username) if err == nil &amp;&amp; cal != nil { return cal, nil } } return c.fetchPublicCalendar(ctx, username)}Enter fullscreen mode Exit fullscreen mode Logarithmic City Layout Algorithm (pkg/city) To prevent single hyper-active weeks from completely distorting the skyline scale, building heights are calculated using logarithmic scaling: // Calculate building heights using logarithmic scale as specified in Quarkus Contribution City:buildingHeights := make([]int, numWeeks)for i, tot := range weeklyTotals { if tot == 0 || maxWeekly == 0 { buildingHeights[i] = 1 } else { ratio := math.Log1p(float64(tot)) / math.Log1p(float64(maxWeekly)) h := 1 + int(math.Round(ratio * float64(opts.MaxHeight-1))) if h &lt; 1 { h = 1 } if h &gt; opts.MaxHeight { h = opts.MaxHeight } buildingHeights[i] = h }}Enter fullscreen mode Exit fullscreen mode Main Application Entrypoint (cmd/city/main.go) The main CLI dispatcher supports generating single SVGs, 3D isometric views, batch processing multiple usernames from an input folder, or starting an embedded HTTP server. package main import ( \"context\" \"encoding/json\" \"flag\" \"fmt\" \"log\" \"os\" \"path/filepath\" \"strings\" \"time\" \"github.com/alainairom/github-contribution-city/pkg/city\" \"github.com/alainairom/github-contribution-city/pkg/config\" \"github.com/alainairom/github-contribution-city/pkg/github\" \"github.com/alainairom/github-contribution-city/pkg/renderer\") func main() { var ( serverMode = flag.Bool(\"server\", false, \"Start the web visualization server\") username = flag.String(\"user\", \"\", \"GitHub username to visualize\") token = flag.String(\"token\", \"\", \"GitHub Personal Access Token (PAT)\") weeks = flag.Int(\"weeks\", 0, \"Number of weeks to render (4 - 53)\") maxHeight = flag.Int(\"height\", 0, \"Maximum building height (3 - 30)\") theme = flag.String(\"theme\", \"\", \"Visual theme\") format = flag.String(\"format\", \"svg\", \"Export format: svg, iso, html, json, all\") outFile = flag.String(\"output\", \"\", \"Output filepath or directory\") port = flag.Int(\"port\", 0, \"HTTP server port\") envFile = flag.String(\"env\", \".env\", \"Path to .env configuration file\") ) flag.Parse() cfg := config.LoadConfig(*envFile) if *username != \"\" { cfg.GitHubUsername = *username } if *token != \"\" { cfg.GitHubToken = *token } if *weeks &gt; 0 { cfg.Weeks = *weeks } if *maxHeight &gt; 0 { cfg.MaxHeight = *maxHeight } if *theme != \"\" { cfg.Theme = *theme } if *port &gt; 0 { cfg.Port = *port } _ = os.MkdirAll(cfg.OutputDir, 0755) client := github.NewClient(cfg.GitHubToken) builder := city.NewBuilder() svgRenderer := renderer.NewSVGRenderer() htmlRenderer := renderer.NewHTMLRenderer() if *serverMode { runServer(cfg, client, builder, svgRenderer, htmlRenderer) return } fmt.Printf(\"🏙️ Generating Contribution City for '%s'...\\n\", cfg.GitHubUsername) ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() cal, err := client.FetchCalendar(ctx, cfg.GitHubUsername) if err != nil { log.Fatalf(\"❌ Error fetching contribution data: %v\", err) } scene := builder.Build(cal, city.Options{ Weeks: cfg.Weeks, MaxHeight: cfg.MaxHeight, Theme: cfg.Theme, }) outPath := filepath.Join(cfg.OutputDir, fmt.Sprintf(\"%s_city.svg\", cfg.GitHubUsername)) svgContent := svgRenderer.RenderSkyline(scene, cfg.Theme) _ = os.WriteFile(outPath, []byte(svgContent), 0644) fmt.Printf(\"✨ SVG Skyline successfully written to: %s\\n\", outPath)}Enter fullscreen mode Exit fullscreen mode Automation with GitHub Actions To keep your contribution city continuously updated without manual intervention, the repository features an automated CI/CD and scheduling suite using GitHub Actions. The workflow automation includes: Continuous Integration (ci.yml): Runs linting checks, go vet, vulnerability scans via govulncheck, multi-OS unit tests (ubuntu, macos, windows), and cross-compilation on every push and pull request. name: Continuous Integration on: push: branches: - main - master paths-ignore: - 'Docs/**' - 'README.md' - 'output/**' pull_request: branches: - main - master permissions: contents: read jobs: lint: name: Lint &amp; Code Style runs-on: ubuntu-latest steps: - name: Checkout Source Code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: '1.24' cache: true - name: Verify Formatting run: | UNFORMATTED=$(gofmt -l . | grep -v '^vendor/' || true) if [ -n \"$UNFORMATTED\" ]; then echo \"❌ Code format issues found in following files:\" echo \"$UNFORMATTED\" exit 1 fi echo \"✅ All Go files formatted properly.\" - name: Run Go Vet run: go vet ./... - name: Run golangci-lint uses: golangci/golangci-lint-action@v6 with: version: latest args: --timeout=5m continue-on-error: true vuln-scan: name: Security &amp; Vulnerability Scan runs-on: ubuntu-latest steps: - name: Checkout Source Code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: '1.24' cache: true - name: Run govulncheck uses: golang/govulncheck-action@v1 with: go-version-input: '1.24' go-package: ./... ...Enter fullscreen mode Exit fullscreen mode Daily Visualization Updates (update-city.yml): Powered by a scheduled cron trigger (03:17 UTC), this workflow fetches fresh contribution matrices for the repository owner and any tracked usernames listed inside the input/ folder, regenerates the respective skylines, and commits the updated graphics back to the repository automatically ([skip ci]). name: Daily Contribution City Update on: schedule: # Runs everyday at 03:17 UTC - cron: '17 3 * * *' workflow_dispatch: inputs: custom_user: description: 'Optional specific GitHub username to generate for' required: false default: '' theme: description: 'Theme to use (github-dark, github-light, cyberpunk, mono, neon, matrix)' required: false default: 'github-dark' permissions: contents: write jobs: update-visualizations: name: Regenerate Contribution Cities runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.24' cache: true - name: Build Contribution City CLI run: | go build -o bin/github-contribution-city cmd/city/main.go - name: Generate Visualizations env: GITHUB_TOKEN: ${{ secrets.CONTRIBUTION_TOKEN || secrets.GITHUB_TOKEN }} run: | mkdir -p output PRIMARY_USER=\"${{ inputs.custom_user || github.repository_owner }}\" SELECTED_THEME=\"${{ inputs.theme || 'github-dark' }}\" echo \"🌆 Generating contribution city for primary user: ${PRIMARY_USER} (Theme: ${SELECTED_THEME})...\" # 1. Generate primary user 2D SVG Skyline ./bin/github-contribution-city \\ -user \"${PRIMARY_USER}\" \\ -weeks 53 \\ -height 14 \\ -theme \"${SELECTED_THEME}\" \\ -format svg \\ -output \"output/contribution-city.svg\" # 2. Generate primary user 3D Isometric SVG ./bin/github-contribution-city \\ -user \"${PRIMARY_USER}\" \\ -weeks 53 \\ -height 14 \\ -theme \"cyberpunk\" \\ -format iso \\ -output \"output/contribution-city-3d.svg\" # 3. Generate primary user 3D Interactive HTML ./bin/github-contribution-city \\ -user \"${PRIMARY_USER}\" \\ -weeks 53 \\ -height 14 \\ -theme \"${SELECTED_THEME}\" \\ -format html \\ -output \"output/contribution-city.html\" # 4. If tracked users exist in input/ directory, batch process them if [ -d \"input\" ] &amp;&amp; [ \"$(ls -A input 2&gt;/dev/null)\" ]; then echo \"📁 Processing batch users from input directory...\" ./bin/github-contribution-city -batch input/ -output output/ fi - name: Commit and Push Changes run: | git config --global user.name \"github-actions[bot]\" git config --global user.email \"github-actions[bot]@users.noreply.github.com\" git add output/ if git diff --staged --quiet; then echo \"ℹ️ No changes detected in contribution calendars. Skipping commit.\" else git commit -m \"chore(city): auto-update contribution city visualizations [skip ci]\" git push echo \"✅ Successfully committed and pushed updated visualizations.\" fi - name: Upload Output Artifacts uses: actions/upload-artifact@v4 with: name: contribution-city-visualizations path: output/ retention-days: 30 Enter fullscreen mode Exit fullscreen mode Container &amp; Release Management (docker.yml &amp; release.yml): Automatically builds multi-arch container images (linux/amd64, linux/arm64) for the GitHub Container Registry (ghcr.io) and packages cross-compiled release binaries whenever version tags are pushed. Conclusion Building GitHub Contribution City was a fun journey merging developer metrics with procedural city generation and graphics rendering. By leveraging Go, we achieved a blazing fast binary that can be embedded directly into CI/CD pipelines, containerized for Docker/Podman, or served live via an interactive Three.js 3D browser experience. Thanks for reading 🌃 Links Github repository for this post: https://github.com/aairom/github-contribution-city Build a GitHub Contribution City With Java and Quarkus (by Markus Eisele): https://dzone.com/articles/build-a-github-contribution-city-with-java-and-qua Quarkus-Contribution-City (java code from Markus-Eisele): https://github.com/myfear/quarkus-contribution-city","contentHash":"sha256:56ce845e8490118a12ec35dc45709bc1d5db763be2ffb7ab552b7c5c8d79d624","authorName":"Alain Airom (Ayrom)","authorUrl":"https://zyvop.com/author/alain","authorSameAs":["https://aairom.github.io/","https://github.com/aairom","https://www.linkedin.com/in/aairom/"],"category":null,"tags":["githubactions","Software","GitHub","cicd","ibmbob"],"audience":"Software engineers and developers building applications with githubactions","tone":"Professional, build engineer perspective","readingTimeMinutes":8,"wordCount":1975,"faqs":null,"primaryTopic":"githubactions","publishedAt":"2026-11-03T09:46:03.000Z","updatedAt":"2026-11-03T09:47:58.943Z","canonicalUrl":"https://dev.to/aairom/building-a-github-contribution-city-transforming-developer-history-into-a-3d-skyline-3hao"}