[{"data":1,"prerenderedAt":408},["ShallowReactive",2],{"project-gha-sheet-attend":3},{"id":4,"title":5,"body":6,"description":389,"duration":390,"extension":391,"featured":392,"github":393,"image":394,"live":395,"meta":396,"navigation":245,"order":337,"path":397,"role":395,"seo":398,"status":399,"stem":400,"team_size":395,"tech":401,"type":405,"year":406,"__hash__":407},"projects\u002Fproject\u002Fgha-sheet-attend.md","GHA Sheet Attendance",{"type":7,"value":8,"toc":381},"minimark",[9,14,18,22,30,58,62,67,72,91,96,170,174,179,182,370,374,377],[10,11,13],"h2",{"id":12},"the-problem","The Problem",[15,16,17],"p",{},"Internships and remote work often require daily attendance logging or \"logbooks.\" Doing this manually in a spreadsheet is repetitive, and maintaining consistent formatting (borders, alignment) across hundreds of rows becomes tedious. Forgetting to log a day can lead to administrative issues at the end of the month.",[10,19,21],{"id":20},"my-solution","My Solution",[15,23,24,25,29],{},"I developed ",[26,27,28],"strong",{},"GHA Sheet Attend",", a \"set-and-forget\" automation tool that turns a GitHub Actions workflow into a structured data entry interface.",[31,32,33,40,46,52],"ul",{},[34,35,36,39],"li",{},[26,37,38],{},"Serverless Data Entry:"," Users can log attendance via a simple form in the GitHub Actions tab (or let it run on autopilot via CRON).",[34,41,42,45],{},[26,43,44],{},"Intelligent Formatting:"," The script doesn't just dump text; it acts as a layout engine, automatically drawing borders and formatting cells for each new entry using the Google Sheets BatchUpdate API.",[34,47,48,51],{},[26,49,50],{},"Smart Defaults:"," Automatically detects weekends or \"Libur\" (Holiday) statuses to clear start\u002Fend times, preventing invalid data entry.",[34,53,54,57],{},[26,55,56],{},"Secure Integration:"," Uses Google Service Account authentication stored in GitHub Secrets, ensuring no credentials are ever exposed in the client-side code.",[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 for a Scripting Task?",[31,73,74,85],{},[34,75,76,79,80,84],{},[26,77,78],{},"Strict Typing for API Payloads:"," The Google Sheets API has nested, complex JSON structures for formatting requests (like ",[81,82,83],"code",{},"UpdateBordersRequest","). Go's struct-based typing makes constructing these payloads significantly less error-prone than untyped languages like JavaScript\u002FPython.",[34,86,87,90],{},[26,88,89],{},"Execution Speed:"," The compiled binary runs instantly on the CI runner, keeping billable action minutes to a minimum (usually under 30 seconds).",[15,92,93],{},[26,94,95],{},"Handling Timezones in CI\u002FCD",[31,97,98],{},[34,99,100,101],{},"GitHub Actions runners default to UTC. To ensure the \"Today\" date in the spreadsheet matches the user's local context (Indonesia), I implemented explicit timezone loading:\n",[102,103,108],"pre",{"className":104,"code":105,"language":106,"meta":107,"style":107},"language-bash shiki shiki-themes github-light github-dark","# go\nloc, _ := time.LoadLocation(\"Asia\u002FJakarta\")\ntoday := time.Now().In(loc)\n","bash","",[81,109,110,119,146],{"__ignoreMap":107},[111,112,115],"span",{"class":113,"line":114},"line",1,[111,116,118],{"class":117},"sJ8bj","# go\n",[111,120,122,126,130,133,136,140,143],{"class":113,"line":121},2,[111,123,125],{"class":124},"sScJk","loc,",[111,127,129],{"class":128},"sZZnC"," _",[111,131,132],{"class":128}," :=",[111,134,135],{"class":128}," time.LoadLocation",[111,137,139],{"class":138},"sVt8B","(",[111,141,142],{"class":124},"\"Asia\u002FJakarta\"",[111,144,145],{"class":138},")\n",[111,147,149,152,154,157,160,163,165,168],{"class":113,"line":148},3,[111,150,151],{"class":124},"today",[111,153,132],{"class":128},[111,155,156],{"class":128}," time.Now",[111,158,159],{"class":138},"()",[111,161,162],{"class":128},".In",[111,164,139],{"class":138},[111,166,167],{"class":124},"loc",[111,169,145],{"class":138},[63,171,173],{"id":172},"key-features-i-built","Key Features I Built",[175,176,178],"h4",{"id":177},"_1-programmatic-layout-engine","1. Programmatic Layout Engine",[15,180,181],{},"Instead of relying on the spreadsheet's conditional formatting (which can break), I engineered the bot to \"draw\" its own table borders after every write operation. It calculates the specific grid range of the newly added row and sends a batch update command.",[102,183,185],{"className":104,"code":184,"language":106,"meta":107,"style":107},"# go\n# Calculate the exact range of the newly added row\nnewRowNumber, _ := strconv.Atoi(matches[1])\nrowIndex := int64(newRowNumber - 1)\n\n$ Send a BatchUpdate request to draw solid borders\nUpdateBorders: &sheets.UpdateBordersRequest{\n    Range: &sheets.GridRange{\n        StartRowIndex: rowIndex,\n        EndRowIndex:   rowIndex + 1,\n        # ...\n    },\n    Top: &sheets.Border{Style: \"SOLID\"},\n    # ...\n}\n",[81,186,187,191,196,215,240,247,277,292,305,314,329,335,341,358,364],{"__ignoreMap":107},[111,188,189],{"class":113,"line":114},[111,190,118],{"class":117},[111,192,193],{"class":113,"line":121},[111,194,195],{"class":117},"# Calculate the exact range of the newly added row\n",[111,197,198,201,203,205,208,210,213],{"class":113,"line":148},[111,199,200],{"class":124},"newRowNumber,",[111,202,129],{"class":128},[111,204,132],{"class":128},[111,206,207],{"class":128}," strconv.Atoi",[111,209,139],{"class":138},[111,211,212],{"class":124},"matches[1]",[111,214,145],{"class":138},[111,216,218,221,223,226,228,231,234,238],{"class":113,"line":217},4,[111,219,220],{"class":124},"rowIndex",[111,222,132],{"class":128},[111,224,225],{"class":128}," int64",[111,227,139],{"class":138},[111,229,230],{"class":124},"newRowNumber",[111,232,233],{"class":128}," -",[111,235,237],{"class":236},"sj4cs"," 1",[111,239,145],{"class":138},[111,241,243],{"class":113,"line":242},5,[111,244,246],{"emptyLinePlaceholder":245},true,"\n",[111,248,250,253,256,259,262,265,268,271,274],{"class":113,"line":249},6,[111,251,252],{"class":124},"$",[111,254,255],{"class":128}," Send",[111,257,258],{"class":128}," a",[111,260,261],{"class":128}," BatchUpdate",[111,263,264],{"class":128}," request",[111,266,267],{"class":128}," to",[111,269,270],{"class":128}," draw",[111,272,273],{"class":128}," solid",[111,275,276],{"class":128}," borders\n",[111,278,280,283,286,289],{"class":113,"line":279},7,[111,281,282],{"class":124},"UpdateBorders:",[111,284,285],{"class":138}," &",[111,287,288],{"class":124},"sheets.UpdateBordersRequest",[111,290,291],{"class":128},"{\n",[111,293,295,298,300,303],{"class":113,"line":294},8,[111,296,297],{"class":124},"    Range:",[111,299,285],{"class":138},[111,301,302],{"class":124},"sheets.GridRange",[111,304,291],{"class":128},[111,306,308,311],{"class":113,"line":307},9,[111,309,310],{"class":124},"        StartRowIndex:",[111,312,313],{"class":128}," rowIndex,\n",[111,315,317,320,323,326],{"class":113,"line":316},10,[111,318,319],{"class":124},"        EndRowIndex:",[111,321,322],{"class":128},"   rowIndex",[111,324,325],{"class":128}," +",[111,327,328],{"class":128}," 1,\n",[111,330,332],{"class":113,"line":331},11,[111,333,334],{"class":117},"        # ...\n",[111,336,338],{"class":113,"line":337},12,[111,339,340],{"class":138},"    },\n",[111,342,344,347,349,352,355],{"class":113,"line":343},13,[111,345,346],{"class":124},"    Top:",[111,348,285],{"class":138},[111,350,351],{"class":124},"sheets.Border",[111,353,354],{"class":128},"{Style:",[111,356,357],{"class":128}," \"SOLID\"},\n",[111,359,361],{"class":113,"line":360},14,[111,362,363],{"class":117},"    # ...\n",[111,365,367],{"class":113,"line":366},15,[111,368,369],{"class":138},"}\n",[175,371,373],{"id":372},"_2-dynamic-sequence-generation","2. Dynamic Sequence Generation",[15,375,376],{},"The system treats the spreadsheet as a database. Before writing, it performs a read operation (Values.Get) to find the last sequence number in Column A, casts it to an integer, and increments it. This ensures the numbering remains sequential (1, 2, 3...) even if rows are manually deleted or modified in between runs.",[378,379,380],"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 .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":107,"searchDepth":121,"depth":121,"links":382},[383,384,385],{"id":12,"depth":121,"text":13},{"id":20,"depth":121,"text":21},{"id":60,"depth":121,"text":61,"children":386},[387,388],{"id":65,"depth":148,"text":66},{"id":172,"depth":148,"text":173},"A serverless attendance logging system that leverages GitHub Actions to automate daily reporting into Google Sheets with programmatic formatting and secure credential handling.","2 weeks","md",false,"https:\u002F\u002Fgithub.com\u002Fszuryuu\u002Fgha-sheet-attend","\u002Fimages\u002Fprojects\u002Fgithub-actions.png",null,{},"\u002Fproject\u002Fgha-sheet-attend",{"title":5,"description":389},"Completed","project\u002Fgha-sheet-attend",[402,403,404],"Go","GitHub Actions","Google Sheets API","Solo Project","2025","yRksOrIhKam8c8OjDwVLYiVvU74ifrxq8D3X4TDpFrw",1776582962970]