[{"data":1,"prerenderedAt":489},["ShallowReactive",2],{"project-gha-follow-unfollow":3},{"id":4,"title":5,"body":6,"description":470,"duration":471,"extension":472,"featured":473,"github":474,"image":475,"live":476,"meta":477,"navigation":208,"order":273,"path":478,"role":476,"seo":479,"status":480,"stem":481,"team_size":476,"tech":482,"type":486,"year":487,"__hash__":488},"projects\u002Fproject\u002Fgha-follow-unfollow.md","GHA Follow Unfollow",{"type":7,"value":8,"toc":462},"minimark",[9,14,18,22,30,58,62,67,72,95,100,118,122,127,130,282,286,289,458],[10,11,13],"h2",{"id":12},"the-problem","The Problem",[15,16,17],"p",{},"Managing GitHub social connections manually is tedious. Many users employ \"follow-for-follow\" strategies only to quietly unfollow later, skewing follower ratios. Manually cross-referencing thousands of followers to find who isn't following back—and reciprocating valid follows—wastes valuable time that could be spent coding.",[10,19,21],{"id":20},"my-solution","My Solution",[15,23,24,25,29],{},"I built ",[26,27,28],"strong",{},"GitHub Follow\u002FUnfollow Bot",", a self-contained automation tool that runs entirely within the GitHub ecosystem, requiring zero external infrastructure or VPS costs.",[31,32,33,40,46,52],"ul",{},[34,35,36,39],"li",{},[26,37,38],{},"Serverless Architecture:"," Leverages GitHub Actions Scheduled Workflows (CRON) to run maintenance tasks automatically twice a day.",[34,41,42,45],{},[26,43,44],{},"Smart Synchronization:"," Automatically detects and follows back new supporters while cleaning up non-mutual connections.",[34,47,48,51],{},[26,49,50],{},"Anti-Abuse Mechanisms:"," Implements random jitter and strict operational limits to mimic human behavior and comply with GitHub's API rate limits.",[34,53,54,57],{},[26,55,56],{},"Audit Logging:"," Automatically commits execution logs back to the repository, creating a permanent history of actions without a database.",[10,59,61],{"id":60},"technical-deep-dive","Technical Deep Dive",[63,64,66],"h3",{"id":65},"architecture-decisions","Architecture Decisions",[15,68,69],{},[26,70,71],{},"Why Go over Python\u002FJS?",[31,73,74,85],{},[34,75,76,79,80,84],{},[26,77,78],{},"Type Safety:"," Leveraging the ",[81,82,83],"code",{},"google\u002Fgo-github"," library ensures strict typing for API responses, preventing runtime errors when handling large follower lists.",[34,86,87,90,91,94],{},[26,88,89],{},"Single Binary Simplicity:"," Although currently run via ",[81,92,93],{},"go run",", the project is structured to be easily compiled into a single binary artifact for portability across different CI runners.",[15,96,97],{},[26,98,99],{},"Why GitHub Actions?",[31,101,102,108],{},[34,103,104,107],{},[26,105,106],{},"Cost Efficiency:"," Replaces the need for a 24\u002F7 server. The script only consumes compute minutes when it runs (approx. 2-5 minutes per run).",[34,109,110,113,114,117],{},[26,111,112],{},"Security:"," Utilizing GitHub Secrets (",[81,115,116],{},"MY_PAT",") ensures high-privilege Personal Access Tokens are injected securely at runtime and never exposed in the codebase.",[63,119,121],{"id":120},"key-features-i-built","Key Features I Built",[123,124,126],"h4",{"id":125},"_1-o1-lookup-strategy-for-difference-calculation","1. O(1) Lookup Strategy for Difference Calculation",[15,128,129],{},"To efficiently handle users with thousands of followers, I utilized Go Maps to implement set difference logic. This reduces the complexity of finding non-mutual followers from O(n²) (nested loops) to O(n).",[131,132,137],"pre",{"className":133,"code":134,"language":135,"meta":136,"style":136},"language-bash shiki shiki-themes github-light github-dark","# go\n# Create a map for O(1) lookups\nfollowingMap := make(map[string]bool)\nfor _, f := range following {\n    followingMap[f.GetLogin()] = true\n}\n\n# Identify followers who aren't followed back (A - B)\nvar needFollow []string\nfor _, f := range followers {\n    if !followingMap[f.GetLogin()] {\n        needFollow = append(needFollow, f.GetLogin())\n    }\n}\n","bash","",[81,138,139,148,154,178,188,197,203,210,216,228,236,248,271,277],{"__ignoreMap":136},[140,141,144],"span",{"class":142,"line":143},"line",1,[140,145,147],{"class":146},"sJ8bj","# go\n",[140,149,151],{"class":142,"line":150},2,[140,152,153],{"class":146},"# Create a map for O(1) lookups\n",[140,155,157,161,165,168,172,175],{"class":142,"line":156},3,[140,158,160],{"class":159},"sScJk","followingMap",[140,162,164],{"class":163},"sZZnC"," :=",[140,166,167],{"class":163}," make",[140,169,171],{"class":170},"sVt8B","(",[140,173,174],{"class":159},"map[string]bool",[140,176,177],{"class":170},")\n",[140,179,181,185],{"class":142,"line":180},4,[140,182,184],{"class":183},"szBVR","for",[140,186,187],{"class":170}," _, f := range following {\n",[140,189,191,194],{"class":142,"line":190},5,[140,192,193],{"class":159},"    followingMap[f.GetLogin",[140,195,196],{"class":170},"()] = true\n",[140,198,200],{"class":142,"line":199},6,[140,201,202],{"class":170},"}\n",[140,204,206],{"class":142,"line":205},7,[140,207,209],{"emptyLinePlaceholder":208},true,"\n",[140,211,213],{"class":142,"line":212},8,[140,214,215],{"class":146},"# Identify followers who aren't followed back (A - B)\n",[140,217,219,222,225],{"class":142,"line":218},9,[140,220,221],{"class":159},"var",[140,223,224],{"class":163}," needFollow",[140,226,227],{"class":170}," []string\n",[140,229,231,233],{"class":142,"line":230},10,[140,232,184],{"class":183},[140,234,235],{"class":170}," _, f := range followers {\n",[140,237,239,242,245],{"class":142,"line":238},11,[140,240,241],{"class":183},"    if",[140,243,244],{"class":159}," !followingMap[f.GetLogin",[140,246,247],{"class":170},"()] {\n",[140,249,251,254,257,260,262,265,268],{"class":142,"line":250},12,[140,252,253],{"class":159},"        needFollow",[140,255,256],{"class":163}," =",[140,258,259],{"class":163}," append",[140,261,171],{"class":170},[140,263,264],{"class":159},"needFollow,",[140,266,267],{"class":163}," f.GetLogin",[140,269,270],{"class":170},"())\n",[140,272,274],{"class":142,"line":273},13,[140,275,276],{"class":170},"    }\n",[140,278,280],{"class":142,"line":279},14,[140,281,202],{"class":170},[123,283,285],{"id":284},"_2-human-like-execution-throttling","2. Human-Like Execution Throttling",[15,287,288],{},"To prevent the account from being flagged as a spam bot, I implemented artificial delays and strict limits. The bot creates a random sleep interval between actions and caps the total operations per run.",[131,290,292],{"className":133,"code":291,"language":135,"meta":136,"style":136},"# go\nconst limit = 25 \u002F\u002F Safety cap per run\n\n\u002F\u002F Random jitter between 2 to 5 seconds\nsleepTime := time.Duration(2+rand.Intn(3)) * time.Second\n\nfor i, user := range needFollow {\n    if i >= limit {\n        log.Printf(\"Reached max follow limit (%d)...\", limit)\n        break\n    }\n    client.FollowPeople(ctx, user)\n    time.Sleep(sleepTime) \u002F\u002F Wait before next action\n}\n",[81,293,294,298,327,331,357,381,385,392,410,425,430,434,444,454],{"__ignoreMap":136},[140,295,296],{"class":142,"line":143},[140,297,147],{"class":146},[140,299,300,303,306,308,312,315,318,321,324],{"class":142,"line":150},[140,301,302],{"class":159},"const",[140,304,305],{"class":163}," limit",[140,307,256],{"class":163},[140,309,311],{"class":310},"sj4cs"," 25",[140,313,314],{"class":163}," \u002F\u002F",[140,316,317],{"class":163}," Safety",[140,319,320],{"class":163}," cap",[140,322,323],{"class":163}," per",[140,325,326],{"class":163}," run\n",[140,328,329],{"class":142,"line":156},[140,330,209],{"emptyLinePlaceholder":208},[140,332,333,336,339,342,345,348,351,354],{"class":142,"line":180},[140,334,335],{"class":159},"\u002F\u002F",[140,337,338],{"class":163}," Random",[140,340,341],{"class":163}," jitter",[140,343,344],{"class":163}," between",[140,346,347],{"class":310}," 2",[140,349,350],{"class":163}," to",[140,352,353],{"class":310}," 5",[140,355,356],{"class":163}," seconds\n",[140,358,359,362,364,367,369,372,375,378],{"class":142,"line":190},[140,360,361],{"class":159},"sleepTime",[140,363,164],{"class":163},[140,365,366],{"class":163}," time.Duration",[140,368,171],{"class":170},[140,370,371],{"class":159},"2+rand.Intn(3",[140,373,374],{"class":170},")) ",[140,376,377],{"class":183},"*",[140,379,380],{"class":170}," time.Second\n",[140,382,383],{"class":142,"line":199},[140,384,209],{"emptyLinePlaceholder":208},[140,386,387,389],{"class":142,"line":205},[140,388,184],{"class":183},[140,390,391],{"class":170}," i, user := range needFollow {\n",[140,393,394,396,399,402,405,407],{"class":142,"line":212},[140,395,241],{"class":183},[140,397,398],{"class":159}," i",[140,400,401],{"class":183}," >",[140,403,404],{"class":163},"=",[140,406,305],{"class":163},[140,408,409],{"class":163}," {\n",[140,411,412,415,418,421,423],{"class":142,"line":218},[140,413,414],{"class":159},"        log.Printf(",[140,416,417],{"class":159},"\"Reached max follow limit (%d)...\"",[140,419,420],{"class":159},",",[140,422,305],{"class":163},[140,424,177],{"class":170},[140,426,427],{"class":142,"line":230},[140,428,429],{"class":183},"        break\n",[140,431,432],{"class":142,"line":238},[140,433,276],{"class":170},[140,435,436,439,442],{"class":142,"line":250},[140,437,438],{"class":159},"    client.FollowPeople(ctx,",[140,440,441],{"class":163}," user",[140,443,177],{"class":170},[140,445,446,449,451],{"class":142,"line":273},[140,447,448],{"class":170},"    time.Sleep(",[140,450,361],{"class":159},[140,452,453],{"class":170},") \u002F\u002F Wait before next action\n",[140,455,456],{"class":142,"line":279},[140,457,202],{"class":170},[459,460,461],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":136,"searchDepth":150,"depth":150,"links":463},[464,465,466],{"id":12,"depth":150,"text":13},{"id":20,"depth":150,"text":21},{"id":60,"depth":150,"text":61,"children":467},[468,469],{"id":65,"depth":156,"text":66},{"id":120,"depth":156,"text":121},"A serverless Go automation tool running on GitHub Actions to manage followers, maintaining a healthy follower ratio with automated sync and activity logging.","2 weeks","md",false,"https:\u002F\u002Fgithub.com\u002Fszuryuu\u002Fgha-follow-unfollow","\u002Fimages\u002Fprojects\u002Fgithub-actions.png",null,{},"\u002Fproject\u002Fgha-follow-unfollow",{"title":5,"description":470},"Completed","project\u002Fgha-follow-unfollow",[483,484,485],"Go","GitHub Actions","GitHub API","Solo Project","2025","KiUEKgKChuOBazKwwHZtjOuDreVHZWFLhIt-37ZvK8k",1776582962984]