-
-
Couldn't load subscription status.
- Fork 12.7k
Add Automated Leaderboard Generation via GitHub Actions #5886
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add Automated Leaderboard Generation via GitHub Actions #5886
Conversation
|
Automated Quality Checks (from CONTRIBUTING minimum standards)
These checks are a best-effort automation and do not replace human review. |
WalkthroughAdds an automated GitHub Actions workflow to update a contributors leaderboard, implements leaderboard generation and contributors page rendering in the static site (new Contributor type, JSON output, template rendering), adds a contributors template and README/CONTRIBUTING docs updates, and updates tests to validate leaderboard generation and rendering. Changes
Sequence Diagram(s)sequenceDiagram
participant GH as GitHub Event (PR merge / manual)
participant Workflow as Update Contributors Workflow
participant Runner as Actions Runner (ubuntu-latest)
participant Go as Go 1.22
participant Site as Static Site (main.go)
participant GitAuto as git-auto-commit-action
GH->>Workflow: PR merged to main / workflow_dispatch
activate Workflow
Workflow->>Runner: spin up runner
Runner->>Workflow: checkout `main` (fetch-depth:0)
Workflow->>Go: setup Go 1.22
Workflow->>Site: run `go run main.go`
activate Site
Site->>Site: generateLeaderboardJSON (call GitHub API, paginate)
Site->>Site: write `leaderboard.json`
Site->>Site: renderContributorsPage (apply `contributors.tmpl.html`)
Site->>Site: write `out/contributors/index.html`
deactivate Site
Workflow->>GitAuto: commit `leaderboard.json` & `out/contributors/index.html`
GitAuto->>Workflow: push to `main` as bot
deactivate Workflow
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Reason: multiple areas touched (CI workflow, core generation logic, template, tests) with new control flow and substantial test changes requiring careful review. Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Hello @avelino, @dukex, @dmitshur, @matrixik, @joeybloggs, @kirillDanshin, @felipeweb, @appleboy, @cassiobotaro, @jtemporal, @ceriath, @panjf2000, @phanirithvij, and @yassinebenaid I’ve opened this pull request to introduce an automated leaderboard generation system powered by GitHub Actions. This pull request resolves discussion #5885, which proposed adding a contributors’ direct showcase section to the project and automating its updates through GitHub Actions. Kindly review this implementation and share your feedback or suggestions for improvement. Thank you all for maintaining this amazing project and for taking the time to review my contribution. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (1)
.github/workflows/update-leaderboard.yml (1)
3-12: Consider optimizing trigger efficiency.The workflow triggers on every PR closed to
main, then filters by the merged status on line 12. This results in unnecessary workflow queue entries for closed non-merged PRs. For a cleaner approach, consider triggering onpushtomaininstead, which naturally occurs only on merged PRs:on: push: branches: - main workflow_dispatch:This eliminates the conditional check and reduces wasted workflow runs. If the current approach is intentional for logging or audit purposes, this optimization can be deferred.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
.github/workflows/update-leaderboard.yml(1 hunks)CONTRIBUTING.md(1 hunks)README.md(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.md
📄 CodeRabbit inference engine (AGENTS.md)
Keep documentation in English
Files:
CONTRIBUTING.mdREADME.md
README.md
📄 CodeRabbit inference engine (AGENTS.md)
README.md: When modifying the Awesome list, keep categories with at least three entries
Descriptions in the Awesome list must end with a period
Avoid promotional copy; keep descriptions concise and neutral in the Awesome list
Do not remove existing list content unless removal is requested and justified
Files:
README.md
{README.md,COVERAGE.md}
📄 CodeRabbit inference engine (AGENTS.md)
Align rendered documentation (README.md, COVERAGE.md, etc.) with behavior changes in main.go or helper packages
Files:
README.md
🧠 Learnings (1)
📚 Learning: 2025-08-14T23:46:18.545Z
Learnt from: elymanuba
PR: avelino/awesome-go#5762
File: README.md:0-0
Timestamp: 2025-08-14T23:46:18.545Z
Learning: When reviewing contributions to the awesome-go repository, maintain a relaxed and less pedantic approach. Users may find overly strict feedback about formatting, alphabetical ordering, or description tone to be unnecessary. Focus on major issues rather than minor stylistic preferences.
Applied to files:
CONTRIBUTING.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (4)
.github/workflows/update-leaderboard.yml (2)
24-31: Node.js setup and caching look good.Using Node.js 20 with npm caching is an appropriate choice that optimizes workflow runtime. No concerns here.
33-36: Verify that the update script exists and handles errors gracefully.The workflow executes
npx ts-node scripts/update-leaderboard.ts, which assumes this script exists at the repository root. If the script fails (e.g., API errors, network issues), the workflow will continue to the commit step even if leaderboard data wasn't updated. Consider adding a check to ensure the script succeeds before committing, or ensure the script exits with a non-zero code on error so the workflow fails appropriately.Verify that:
scripts/update-leaderboard.tsexists and is included in this PR- The script handles errors and exits with appropriate status codes (non-zero on failure)
- The script behaves correctly when invoked with the
GITHUB_TOKENenvironment variableCONTRIBUTING.md (1)
4-4: Verify that the README.md anchor target exists.The link
./README.md/#contributionassumes a corresponding anchor or heading with idcontributionexists in README.md. Per the PR summary, a Contributors badge section should be present. Ensure the anchor is correctly named and that the link resolves properly when viewed on GitHub.Following the learnings from prior reviews, I'm keeping feedback focused on functional correctness rather than stylistic preferences. This change appropriately directs contributors to the internal README section.
Confirm that README.md contains a section with id
#contribution(or equivalent heading that GitHub will auto-link to).README.md (1)
3788-3795: Contributors badge looks greatThe new badge + avatar strip render cleanly, link to the repo’s contributors graph, and reinforce the project’s community focus. Nice touch!
| - name: "Commit and push changes" | ||
| uses: stefanzweifel/git-auto-commit-action@v5 | ||
| with: | ||
| commit_message: "docs(leaderboard): update leaderboard.json" | ||
| file_pattern: "apps/web/public/leaderboard.json" | ||
| branch: main | ||
| commit_user_name: "github-actions[bot]" | ||
| commit_user_email: "github-actions[bot]@users.noreply.github.com" | ||
| commit_author: "github-actions[bot] <github-actions[bot]@users.noreply.github.com>" No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Third-party action configuration is sound.
The git-auto-commit-action setup is appropriate, with:
- Specific version pin (v5) avoiding unexpected breaking changes
- File pattern limiting scope to leaderboard.json
- Proper commit metadata and bot attribution
- Correct branch target (main)
Ensure that stefanzweifel/git-auto-commit-action@v5 remains maintained and is compatible with your GitHub Actions environment. Consider periodically checking for updates, but the current pinned version provides stability.
🌐 Web query:
Is stefanzweifel/git-auto-commit-action@v5 currently maintained and is it the latest stable version?
💡 Result:
No — @v5 is not the latest. The action is actively maintained (recent releases/commits) and the current major/stable release is v6 (latest release v6.0.1). Use stefanzweifel/git-auto-commit-action@v6 to track the current stable series. [1][2]
Sources:
[1] GitHub repo (stefanzweifel/git-auto-commit-action).
[2] Release summary showing latest release v6.0.1.
Update git-auto-commit-action to v6 (current stable release).
The action is actively maintained, but v6 is the current major/stable release (v6.0.1 is the latest). Update stefanzweifel/git-auto-commit-action@v5 to @v6 to stay on the current stable series.
The rest of the configuration (file pattern, commit metadata, bot attribution, branch) remains sound.
🤖 Prompt for AI Agents
.github/workflows/update-leaderboard.yml lines 38-46: update the GitHub Action
reference from stefanzweifel/git-auto-commit-action@v5 to
stefanzweifel/git-auto-commit-action@v6; keep the existing inputs
(commit_message, file_pattern, branch, commit_user_name, commit_user_email,
commit_author) unchanged so the behaviour and metadata remain the same while
moving to the current stable major release.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
main.go (1)
271-278: Close sitemap file and return any close errorPrevent descriptor leaks and ensure data is flushed.
Apply this diff:
func renderSitemap(categories map[string]Category) error { f, err := os.Create(outSitemapFile) if err != nil { return fmt.Errorf("create sitemap file `%s`: %w", outSitemapFile, err) } fmt.Printf("Render Sitemap to: %s\n", outSitemapFile) - return tpl.Lookup("sitemap.tmpl.xml").Execute(f, categories) + if err := tpl.Lookup("sitemap.tmpl.xml").Execute(f, categories); err != nil { + _ = f.Close() + return err + } + return f.Close() }
♻️ Duplicate comments (1)
.github/workflows/update-leaderboard.yml (1)
35-35: Use git-auto-commit-action v6 (current stable) instead of v5This mirrors prior feedback; keep inputs unchanged.
Apply this diff:
- - name: Commit and push leaderboard + contributors page - uses: stefanzweifel/git-auto-commit-action@v5 + - name: Commit and push leaderboard + contributors page + uses: stefanzweifel/git-auto-commit-action@v6
🧹 Nitpick comments (3)
.github/workflows/update-leaderboard.yml (1)
10-18: Optional: add concurrency to avoid races on rapid mergesGuard against concurrent runs pushing overlapping commits.
Example:
jobs: build-site: + concurrency: + group: update-leaderboard + cancel-in-progress: falsetmpl/contributors.tmpl.html (1)
137-151: Optional a11y: add aria-labels for contributor linksImprove screen-reader context: aria-label="{{ .Login }}: {{ .Contributions }} contributions".
main.go (1)
189-201: Fallback avatar for anonymous contributors may 404avatars.githubusercontent.com/ isn’t guaranteed for non-user placeholders. Use a known default (e.g., https://github.com/ghost.png).
Apply this diff:
- if c.AvatarURL == "" { - // Use GitHub-style default identicon based on login - pageContributors[i].AvatarURL = fmt.Sprintf( - "https://avatars.githubusercontent.com/%s?v=4", - url.PathEscape(pageContributors[i].Login), - ) - } + if c.AvatarURL == "" { + pageContributors[i].AvatarURL = "https://github.com/ghost.png" + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
.github/workflows/update-leaderboard.yml(1 hunks)README.md(1 hunks)main.go(10 hunks)main_test.go(1 hunks)tmpl/contributors.tmpl.html(1 hunks)tmpl/index.tmpl.html(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- README.md
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Use standard formatting (gofmt) and idiomatic Go style
Favor small, testable functions in Go
Keep exported Go APIs documented with Go-style comments
Maintain ≥80% coverage for non-data packages and ≥90% for data packages when adding new testable Go code
Files:
main_test.gomain.go
{**/*.go,.github/scripts/**/*.js}
📄 CodeRabbit inference engine (AGENTS.md)
Use American English spelling in code comments
Files:
main_test.gomain.go
🧠 Learnings (1)
📚 Learning: 2025-10-08T00:38:13.132Z
Learnt from: CR
PR: avelino/awesome-go#0
File: AGENTS.md:0-0
Timestamp: 2025-10-08T00:38:13.132Z
Learning: Applies to {README.md,COVERAGE.md} : Align rendered documentation (README.md, COVERAGE.md, etc.) with behavior changes in main.go or helper packages
Applied to files:
main_test.go
🧬 Code graph analysis (1)
main_test.go (1)
main.go (1)
Contributor(40-45)
🪛 actionlint (1.7.8)
.github/workflows/update-leaderboard.yml
27-27: the runner of "actions/setup-go@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Codacy Static Code Analysis
| uses: actions/setup-go@v4 | ||
| with: | ||
| go-version: "1.22" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update actions/setup-go to v5 (per actionlint hint)
actionlint warns v4 is too old for runners. Bump to the current major.
Apply this diff:
- - name: Set up Go
- uses: actions/setup-go@v4
+ - name: Set up Go
+ uses: actions/setup-go@v5
with:
go-version: "1.22"Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 actionlint (1.7.8)
27-27: the runner of "actions/setup-go@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
🤖 Prompt for AI Agents
In .github/workflows/update-leaderboard.yml around lines 27 to 29, update the
GitHub Actions step using actions/setup-go from v4 to v5 per actionlint's
recommendation; modify the uses value to actions/setup-go@v5 while keeping the
existing with.go-version setting (e.g., "1.22") unchanged so the runner uses the
newer major of the action.
| - name: Build static site | ||
| run: go run main.go | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pass GITHUB_TOKEN to avoid GitHub API rate limits during build
generateLeaderboardJSON reads GITHUB_TOKEN from env, but the step doesn’t export it. Without it, the API calls will be unauthenticated and can hit rate limits on large repos.
Apply this diff:
- - name: Build static site
- run: go run main.go
+ - name: Build static site
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: go run main.go📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Build static site | |
| run: go run main.go | |
| - name: Build static site | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: go run main.go | |
🤖 Prompt for AI Agents
In .github/workflows/update-leaderboard.yml around lines 31-33, the "Build
static site" step runs `go run main.go` but does not pass GITHUB_TOKEN, so
generateLeaderboardJSON will make unauthenticated API calls; update that step to
export the token by adding an env mapping GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN
}} (or set env at job level) so the process reads the token from the workflow
secrets and avoids GitHub API rate limits.
| file_pattern: | | ||
| leaderboard.json | ||
| out/contributors/index.html | ||
| branch: main |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong commit path: leaderboard.json lives under out/
main.go writes leaderboardJSON to out/leaderboard.json. The workflow commits leaderboard.json (repo root), so changes won’t be picked up.
Apply this diff:
with:
commit_message: "docs: update leaderboard & contributors"
file_pattern: |
- leaderboard.json
+ out/leaderboard.json
out/contributors/index.html
branch: main🤖 Prompt for AI Agents
In .github/workflows/update-leaderboard.yml around lines 38 to 41, the workflow
is committing leaderboard.json at the repo root but main.go writes
out/leaderboard.json; update the file_pattern to point to the correct path
(out/leaderboard.json) so the workflow picks up and commits the generated file
(keep out/contributors/index.html entry as-is).
| func TestLeaderboardGeneration(t *testing.T) { | ||
| // Use mock leaderboard for safe testing | ||
| os.Setenv("TEST_LEADERBOARD", "1") | ||
| defer os.Unsetenv("TEST_LEADERBOARD") | ||
|
|
||
| lines := strings.Split(string(input), "\n") | ||
| for _, line := range lines { | ||
| line = strings.Trim(line, " ") | ||
| containsLink = reContainsLink.MatchString(line) | ||
| if containsLink { | ||
| noDescription = reOnlyLink.MatchString(line) | ||
| if noDescription { | ||
| continue | ||
| } | ||
| matched = reLinkWithDescription.MatchString(line) | ||
| if !matched { | ||
| t.Errorf("expected entry to be in form of `* [link] - description.`, got '%s'", line) | ||
| } | ||
| } | ||
| // Build static site | ||
| if err := buildStaticSite(); err != nil { | ||
| t.Fatalf("buildStaticSite failed: %v", err) | ||
| } | ||
| } | ||
|
|
||
| func TestRenderIndex(t *testing.T) { | ||
| requireNoErr(t, mkdirAll(outDir), "output dir should exists") | ||
|
|
||
| err := renderIndex(readmePath, outIndexFile) | ||
| requireNoErr(t, err, "html should be rendered") | ||
| } | ||
| // Check leaderboard.json exists | ||
| info, err := os.Stat(leaderboardJSON) | ||
| if err != nil { | ||
| t.Fatalf("leaderboard.json missing: %v", err) | ||
| } | ||
| if info.Size() == 0 { | ||
| t.Fatal("leaderboard.json is empty") | ||
| } | ||
|
|
||
| func testList(t *testing.T, list *goquery.Selection) { | ||
| list.Find("ul").Each(func(_ int, items *goquery.Selection) { | ||
| testList(t, items) | ||
| items.RemoveFiltered("ul") | ||
| }) | ||
| t.Run(list.Prev().Text(), func(t *testing.T) { | ||
| checkAlphabeticOrder(t, list) | ||
| }) | ||
| } | ||
| // Read and validate JSON | ||
| data, err := os.ReadFile(leaderboardJSON) | ||
| if err != nil { | ||
| t.Fatalf("read leaderboard.json failed: %v", err) | ||
| } | ||
|
|
||
| func checkAlphabeticOrder(t *testing.T, s *goquery.Selection) { | ||
| items := s.Find("li > a:first-child").Map(func(_ int, li *goquery.Selection) string { | ||
| return strings.ToLower(li.Text()) | ||
| }) | ||
| sorted := make([]string, len(items)) | ||
| copy(sorted, items) | ||
| sort.Strings(sorted) | ||
| for k, item := range items { | ||
| if item != sorted[k] { | ||
| t.Errorf("expected '%s' but actual is '%s'", sorted[k], item) | ||
| } | ||
| var contributors []Contributor | ||
| if err := json.Unmarshal(data, &contributors); err != nil { | ||
| t.Fatalf("invalid leaderboard JSON: %v", err) | ||
| } | ||
| if len(contributors) == 0 { | ||
| t.Fatal("no contributors found in leaderboard.json") | ||
| } | ||
| if t.Failed() { | ||
| t.Logf("expected order is:\n%s", strings.Join(sorted, "\n")) | ||
|
|
||
| // Validate first mock contributor | ||
| first := contributors[0] | ||
| if first.Login == "" || first.Contributions == 0 { | ||
| t.Errorf("first contributor missing required fields: %+v", first) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Tests currently hit the live GitHub API; rely on a mock path instead
As written, this test will flake/ratelimit because generateLeaderboardJSON ignores TEST_LEADERBOARD. With the proposed code change, it becomes hermetic and stable.
Run to confirm TEST_LEADERBOARD isn’t referenced outside tests:
Expected: only matches in main_test.go after the code change adds the check in generateLeaderboardJSON.
🏁 Script executed:
#!/bin/bash
rg -n "TEST_LEADERBOARD" -C2Length of output: 282
---
🏁 Script executed:
```shell
#!/bin/bash
# Search for generateLeaderboardJSON function implementation
rg -n "func generateLeaderboardJSON|TEST_LEADERBOARD" --type=go -C5
Length of output: 926
---
🏁 Script executed:
```shell
#!/bin/bash
# Extract generateLeaderboardJSON function from main.go
# Find function start at line 152 and show next ~50 lines or until next func
sed -n '152,220p' main.go
Length of output: 1936
generateLeaderboardJSON must check TEST_LEADERBOARD and skip live API calls during testing
The test sets TEST_LEADERBOARD=1 but generateLeaderboardJSON (main.go:152) unconditionally fetches from GitHub API, causing the test to hit the network and flake on rate limits. Add a check at the start of generateLeaderboardJSON:
if os.Getenv("TEST_LEADERBOARD") == "1" {
// Use mock contributors for testing instead of live API
// ... load or construct test data ...
return nil
}This must be implemented before the HTTP request at line 161.
🤖 Prompt for AI Agents
In main.go around lines 152 to 161, generateLeaderboardJSON currently always
calls the GitHub API; add an early check for TEST_LEADERBOARD
(os.Getenv("TEST_LEADERBOARD") == "1") before the HTTP request and short-circuit
to use or construct mock contributor data and write the same leaderboardJSON
output the test expects (or return the appropriate value) so the function does
not perform live network calls during tests; ensure the mock data matches the
Contributor structure and that leaderboardJSON is produced so
TestLeaderboardGeneration can read and validate it.
| var tpl = template.Must(template.New("").Funcs(template2.FuncMap{ | ||
| "min": func(a, b int) int { | ||
| if a < b { | ||
| return a | ||
| } | ||
| return b | ||
| }, | ||
| "add": func(nums ...int) int { | ||
| sum := 0 | ||
| for _, n := range nums { | ||
| sum += n | ||
| } | ||
| return sum | ||
| }, | ||
| }).ParseFS(tplFs, "tmpl/*.tmpl.html", "tmpl/*.tmpl.xml")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FuncMap type mismatch: use text/template.FuncMap (or switch fully to html/template)
You pass html/template.FuncMap to text/template.Funcs, which is a different type and won’t compile. Use template.FuncMap here. Optionally, consider moving all HTML templates to html/template for auto-escaping.
Apply this minimal fix:
-var tpl = template.Must(template.New("").Funcs(template2.FuncMap{
+var tpl = template.Must(template.New("").Funcs(template.FuncMap{
"min": func(a, b int) int {
@@
},
}).ParseFS(tplFs, "tmpl/*.tmpl.html", "tmpl/*.tmpl.xml"))If you’d like, I can propose a follow-up to split HTML (html/template) and XML (text/template) rendering.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| var tpl = template.Must(template.New("").Funcs(template2.FuncMap{ | |
| "min": func(a, b int) int { | |
| if a < b { | |
| return a | |
| } | |
| return b | |
| }, | |
| "add": func(nums ...int) int { | |
| sum := 0 | |
| for _, n := range nums { | |
| sum += n | |
| } | |
| return sum | |
| }, | |
| }).ParseFS(tplFs, "tmpl/*.tmpl.html", "tmpl/*.tmpl.xml")) | |
| var tpl = template.Must(template.New("").Funcs(template.FuncMap{ | |
| "min": func(a, b int) int { | |
| if a < b { | |
| return a | |
| } | |
| return b | |
| }, | |
| "add": func(nums ...int) int { | |
| sum := 0 | |
| for _, n := range nums { | |
| sum += n | |
| } | |
| return sum | |
| }, | |
| }).ParseFS(tplFs, "tmpl/*.tmpl.html", "tmpl/*.tmpl.xml")) |
🤖 Prompt for AI Agents
In main.go around lines 60 to 74, the FuncMap passed to template.Funcs uses the
wrong type (html/template.FuncMap via template2); replace that with
template.FuncMap so the types match (or alternatively switch the whole file to
html/template and adjust imports). Concretely, change the FuncMap construction
to use template.FuncMap (and adjust the import aliases if needed) so
template.Funcs receives the correct type and the code compiles.
| // ---------------------- Generate Leaderboard JSON via GitHub API ---------------------- | ||
|
|
||
| func generateLeaderboardJSON() error { | ||
| repoOwner := "avelino" | ||
| repoName := "awesome-go" | ||
|
|
||
| var contributors []Contributor | ||
| page := 1 | ||
|
|
||
| for { | ||
| apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/contributors?per_page=100&anon=1&page=%d", repoOwner, repoName, page) | ||
| req, _ := http.NewRequestWithContext(context.Background(), "GET", apiURL, nil) | ||
|
|
||
| if token := os.Getenv("GITHUB_TOKEN"); token != "" { | ||
| req.Header.Set("Authorization", "token "+token) | ||
| } | ||
|
|
||
| resp, err := http.DefaultClient.Do(req) | ||
| if err != nil { | ||
| return fmt.Errorf("fetch contributors: %w", err) | ||
| } | ||
| if resp.Body != nil { | ||
| defer resp.Body.Close() | ||
| } | ||
|
|
||
| if resp.StatusCode != 200 { | ||
| return fmt.Errorf("github api error: status %d", resp.StatusCode) | ||
| } | ||
|
|
||
| var pageContributors []Contributor | ||
| decoder := json.NewDecoder(resp.Body) | ||
| if err := decoder.Decode(&pageContributors); err != nil { | ||
| return fmt.Errorf("decode github contributors: %w", err) | ||
| } | ||
|
|
||
| if len(pageContributors) == 0 { | ||
| break | ||
| } | ||
|
|
||
| for i, c := range pageContributors { | ||
| if c.Login == "" { | ||
| pageContributors[i].Login = "Anonymous Contributor" | ||
| pageContributors[i].HTMLURL = "#" | ||
| } | ||
| if c.AvatarURL == "" { | ||
| // Use GitHub-style default identicon based on login | ||
| pageContributors[i].AvatarURL = fmt.Sprintf( | ||
| "https://avatars.githubusercontent.com/%s?v=4", | ||
| url.PathEscape(pageContributors[i].Login), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| contributors = append(contributors, pageContributors...) | ||
| page++ | ||
| } | ||
|
|
||
| if err := mkdirAll(dir); err != nil { | ||
| return fmt.Errorf("create dir: %w", err) | ||
| if len(contributors) == 0 { | ||
| return errors.New("no contributors found from GitHub API") | ||
| } | ||
|
|
||
| data, _ := json.MarshalIndent(contributors, "", " ") | ||
| if err := os.WriteFile(leaderboardJSON, data, 0644); err != nil { | ||
| return fmt.Errorf("write leaderboard.json: %w", err) | ||
| } | ||
|
|
||
| fmt.Printf("✅ Generated leaderboard.json with %d contributors → %s\n", len(contributors), leaderboardJSON) | ||
| return nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Harden GitHub API calls: fix response body handling and basic error checks
- defer Close inside a loop leaks descriptors until function returns.
- NewRequest error is ignored.
- Marshal error is ignored.
Apply this diff:
func generateLeaderboardJSON() error {
@@
- req, _ := http.NewRequestWithContext(context.Background(), "GET", apiURL, nil)
+ req, err := http.NewRequestWithContext(context.Background(), "GET", apiURL, nil)
+ if err != nil {
+ return fmt.Errorf("build request: %w", err)
+ }
+ req.Header.Set("User-Agent", "awesome-go-leaderboard/1.0")
@@
- if resp.Body != nil {
- defer resp.Body.Close()
- }
+ // Read and close body within the iteration
+ if resp.Body == nil {
+ return fmt.Errorf("empty response body: status %d", resp.StatusCode)
+ }
+ defer func(b io.ReadCloser) { _ = b.Close() }(resp.Body)
@@
- if err := decoder.Decode(&pageContributors); err != nil {
+ if err := decoder.Decode(&pageContributors); err != nil {
return fmt.Errorf("decode github contributors: %w", err)
}
+ _ = resp.Body.Close()
@@
- data, _ := json.MarshalIndent(contributors, "", " ")
+ data, err := json.MarshalIndent(contributors, "", " ")
+ if err != nil {
+ return fmt.Errorf("marshal leaderboard: %w", err)
+ }Optionally, move http.DefaultClient to a client with a timeout and add basic 403 rate‑limit backoff.
🤖 Prompt for AI Agents
main.go around lines 150 to 217: fix several robustness issues when calling the
GitHub API: check and return the error from http.NewRequestWithContext instead
of ignoring it; ensure resp.Body is closed immediately per-iteration (do not use
a loop-level defer that accumulates — either call resp.Body.Close() explicitly
after reading, or wrap the per-page logic in a small func and defer inside it so
the body is closed each iteration); handle json.MarshalIndent error instead of
discarding it; replace http.DefaultClient with a client that has a reasonable
timeout and add a simple 403-rate-limit backoff/retry (detect StatusCode 403 and
Retry-After or exponential backoff with a few retries) before failing. Ensure
all error messages include context when returned.
| func renderContributorsPage() error { | ||
| // Read leaderboard JSON | ||
| data, err := os.ReadFile(leaderboardJSON) | ||
| if err != nil { | ||
| return fmt.Errorf("read leaderboard.json: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| var contributors []Contributor | ||
| if err := json.Unmarshal(data, &contributors); err != nil { | ||
| return fmt.Errorf("parse leaderboard.json: %w", err) | ||
| } | ||
|
|
||
| if contributors == nil { | ||
| contributors = []Contributor{} | ||
| } | ||
|
|
||
| // Split contributors into sections | ||
| top3 := contributors | ||
| if len(top3) > 3 { | ||
| top3 = contributors[:3] | ||
| } | ||
|
|
||
| top10 := []Contributor{} | ||
| if len(contributors) > 3 { | ||
| if len(contributors) > 13 { | ||
| top10 = contributors[3:13] | ||
| } else { | ||
| top10 = contributors[3:] | ||
| } | ||
| } | ||
|
|
||
| top100 := []Contributor{} | ||
| if len(contributors) > 13 { | ||
| if len(contributors) > 113 { | ||
| top100 = contributors[13:113] | ||
| } else { | ||
| top100 = contributors[13:] | ||
| } | ||
| } | ||
|
|
||
| others := []Contributor{} | ||
| if len(contributors) > 113 { | ||
| others = contributors[113:] | ||
| } | ||
|
|
||
| // Ensure output directory exists | ||
| if err := mkdirAll(outContributorsDir); err != nil { | ||
| return fmt.Errorf("create contributors dir: %w", err) | ||
| } | ||
|
|
||
| // Create HTML file | ||
| outFile := filepath.Join(outContributorsDir, "index.html") | ||
| f, err := os.Create(outFile) | ||
| if err != nil { | ||
| return fmt.Errorf("create contributors file: %w", err) | ||
| } | ||
| defer f.Close() | ||
|
|
||
| fmt.Println("Render Contributors Leaderboard →", outFile) | ||
|
|
||
| // Execute template | ||
| return tpl.Lookup("contributors.tmpl.html").Execute(f, map[string]interface{}{ | ||
| "Top3": top3, | ||
| "Top10": top10, | ||
| "Top100": top100, | ||
| "Others": others, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implement test mode to avoid live network calls in CI
Tests set TEST_LEADERBOARD=1, but generateLeaderboardJSON doesn’t honor it. Provide a small in-process mock path to keep tests hermetic.
Apply this diff near the start of generateLeaderboardJSON:
func generateLeaderboardJSON() error {
+ if os.Getenv("TEST_LEADERBOARD") == "1" {
+ mock := []Contributor{
+ {Login: "alice", AvatarURL: "https://avatars.githubusercontent.com/u/1?v=4", HTMLURL: "https://github.com/alice", Contributions: 100},
+ {Login: "bob", AvatarURL: "https://avatars.githubusercontent.com/u/2?v=4", HTMLURL: "https://github.com/bob", Contributions: 80},
+ {Login: "carol", AvatarURL: "https://avatars.githubusercontent.com/u/3?v=4", HTMLURL: "https://github.com/carol", Contributions: 60},
+ {Login: "dave", AvatarURL: "https://avatars.githubusercontent.com/u/4?v=4", HTMLURL: "https://github.com/dave", Contributions: 40},
+ }
+ data, err := json.MarshalIndent(mock, "", " ")
+ if err != nil {
+ return fmt.Errorf("marshal mock leaderboard: %w", err)
+ }
+ if err := os.WriteFile(leaderboardJSON, data, 0644); err != nil {
+ return fmt.Errorf("write mock leaderboard.json: %w", err)
+ }
+ return nil
+ }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In main.go around lines 282-349, add support for TEST_LEADERBOARD to make
generateLeaderboardJSON hermetic during CI by short-circuiting live network
calls: if os.Getenv("TEST_LEADERBOARD") == "1" then write a small deterministic
mock leaderboard JSON to a local temp/test fixture path (or set leaderboardJSON
to that path) and return that path immediately instead of performing network
requests; ensure generateLeaderboardJSON uses that path so the rest of
renderContributorsPage can read it, and keep normal network behavior when
TEST_LEADERBOARD is not set.
| linkURL, err := url.Parse(href) | ||
| if err == nil && linkURL.Fragment != "" && linkURL.Fragment != "contents" { | ||
| s.SetAttr("href", linkURL.Fragment) | ||
| } | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anchor rewriting drops the leading #
Setting href to linkURL.Fragment removes the ‘#’, breaking in-page navigation.
Apply this diff:
- if err == nil && linkURL.Fragment != "" && linkURL.Fragment != "contents" {
- s.SetAttr("href", linkURL.Fragment)
- }
+ if err == nil && linkURL.Fragment != "" && linkURL.Fragment != "contents" {
+ s.SetAttr("href", "#"+linkURL.Fragment)
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| linkURL, err := url.Parse(href) | |
| if err == nil && linkURL.Fragment != "" && linkURL.Fragment != "contents" { | |
| s.SetAttr("href", linkURL.Fragment) | |
| } | |
| }) | |
| linkURL, err := url.Parse(href) | |
| if err == nil && linkURL.Fragment != "" && linkURL.Fragment != "contents" { | |
| s.SetAttr("href", "#"+linkURL.Fragment) | |
| } | |
| }) |
🤖 Prompt for AI Agents
In main.go around lines 436 to 440, the anchor rewriting sets href to
linkURL.Fragment which drops the leading ‘#’ and breaks in-page navigation;
update the assignment to prepend a ‘#’ when setting href (e.g., set href to "#"
+ linkURL.Fragment) so the resulting href remains a valid in-page anchor,
keeping the existing checks for non-empty and non-"contents" fragments.
| <a href="{{ $c.HTMLURL }}" target="_blank" class="card {{if eq $index 0}}top1{{else if eq $index 1}}top2{{else if eq $index 2}}top3{{end}}"> | ||
| <div class="medal">{{ add $index 1 }}</div> | ||
| <img src="{{ $c.AvatarURL }}" alt="{{ $c.Login }}"> | ||
| <h3>{{ $c.Login }}</h3> | ||
| <p>Contributions: {{ $c.Contributions }}</p> | ||
| </a> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add rel="noopener noreferrer" on external links opened in new tabs
Prevent tabnabbing and reduce window.opener risks.
Apply this diff:
- <a href="{{ $c.HTMLURL }}" target="_blank" class="card {{if eq $index 0}}top1{{else if eq $index 1}}top2{{else if eq $index 2}}top3{{end}}">
+ <a href="{{ $c.HTMLURL }}" target="_blank" rel="noopener noreferrer" class="card {{if eq $index 0}}top1{{else if eq $index 1}}top2{{else if eq $index 2}}top3{{end}}">
@@
- <a href="{{ .HTMLURL }}" target="_blank" class="card">
+ <a href="{{ .HTMLURL }}" target="_blank" rel="noopener noreferrer" class="card">
@@
- <a href="{{ .HTMLURL }}" target="_blank" class="card">
+ <a href="{{ .HTMLURL }}" target="_blank" rel="noopener noreferrer" class="card">
@@
- <a href="{{ .HTMLURL }}" target="_blank" class="card">
+ <a href="{{ .HTMLURL }}" target="_blank" rel="noopener noreferrer" class="card">Also applies to: 160-164, 173-177, 186-190
🤖 Prompt for AI Agents
In tmpl/contributors.tmpl.html around lines 146-151 (and also update occurrences
at 160-164, 173-177, 186-190), the anchor tags use target="_blank" without rel
attributes; add rel="noopener noreferrer" to each external link opened in a new
tab to prevent tabnabbing and remove access to window.opener. Modify each <a ...
target="_blank"> to include rel="noopener noreferrer" while preserving existing
classes and attributes.
| <link rel="stylesheet" type="text/css" href="/tmpl/assets/fonts/firasans.css" /> | ||
| <link rel="stylesheet" type="text/css" href="/tmpl/assets/normalize.css" /> | ||
| <link rel="stylesheet" type="text/css" href="/tmpl/assets/awesome-go.css" /> | ||
|
|
||
| <!--ICONS--> | ||
| <link rel="icon" href="/assets/favicon/favicon.ico" type="image/x-icon" /> | ||
| <link rel="apple-touch-icon" href="/assets/favicon/apple-touch-icon.png" /> | ||
| <link rel="manifest" href="/assets/favicon/manifest.json" /> | ||
| <link rel="icon" href="/tmpl/assets/favicon/favicon.ico" type="image/x-icon" /> | ||
| <link rel="apple-touch-icon" href="/tmpl/assets/favicon/apple-touch-icon.png" /> | ||
| <link rel="manifest" href="/tmpl/assets/favicon/manifest.json" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Broken asset paths: use /assets not /tmpl/assets
Templates reference /tmpl/assets/* but the build copies tmpl/assets → out/assets. On the site, /tmpl/assets won’t exist, breaking CSS and icons. Point to /assets/*.
Apply this diff:
- <link rel="stylesheet" type="text/css" href="/tmpl/assets/fonts/firasans.css" />
- <link rel="stylesheet" type="text/css" href="/tmpl/assets/normalize.css" />
- <link rel="stylesheet" type="text/css" href="/tmpl/assets/awesome-go.css" />
+ <link rel="stylesheet" type="text/css" href="/assets/fonts/firasans.css" />
+ <link rel="stylesheet" type="text/css" href="/assets/normalize.css" />
+ <link rel="stylesheet" type="text/css" href="/assets/awesome-go.css" />
@@
- <link rel="icon" href="/tmpl/assets/favicon/favicon.ico" type="image/x-icon" />
- <link rel="apple-touch-icon" href="/tmpl/assets/favicon/apple-touch-icon.png" />
- <link rel="manifest" href="/tmpl/assets/favicon/manifest.json" />
+ <link rel="icon" href="/assets/favicon/favicon.ico" type="image/x-icon" />
+ <link rel="apple-touch-icon" href="/assets/favicon/apple-touch-icon.png" />
+ <link rel="manifest" href="/assets/favicon/manifest.json" />📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <link rel="stylesheet" type="text/css" href="/tmpl/assets/fonts/firasans.css" /> | |
| <link rel="stylesheet" type="text/css" href="/tmpl/assets/normalize.css" /> | |
| <link rel="stylesheet" type="text/css" href="/tmpl/assets/awesome-go.css" /> | |
| <!--ICONS--> | |
| <link rel="icon" href="/assets/favicon/favicon.ico" type="image/x-icon" /> | |
| <link rel="apple-touch-icon" href="/assets/favicon/apple-touch-icon.png" /> | |
| <link rel="manifest" href="/assets/favicon/manifest.json" /> | |
| <link rel="icon" href="/tmpl/assets/favicon/favicon.ico" type="image/x-icon" /> | |
| <link rel="apple-touch-icon" href="/tmpl/assets/favicon/apple-touch-icon.png" /> | |
| <link rel="manifest" href="/tmpl/assets/favicon/manifest.json" /> | |
| <link rel="stylesheet" type="text/css" href="/assets/fonts/firasans.css" /> | |
| <link rel="stylesheet" type="text/css" href="/assets/normalize.css" /> | |
| <link rel="stylesheet" type="text/css" href="/assets/awesome-go.css" /> | |
| <!--ICONS--> | |
| <link rel="icon" href="/assets/favicon/favicon.ico" type="image/x-icon" /> | |
| <link rel="apple-touch-icon" href="/assets/favicon/apple-touch-icon.png" /> | |
| <link rel="manifest" href="/assets/favicon/manifest.json" /> |
🤖 Prompt for AI Agents
In tmpl/index.tmpl.html around lines 20 to 27 the asset references use
/tmpl/assets/* but the build copies tmpl/assets to out/assets so those paths
will 404; update each href/src/manifest path to point to /assets/* (e.g.
/assets/fonts/firasans.css, /assets/normalize.css, /assets/awesome-go.css,
/assets/favicon/favicon.ico, /assets/favicon/apple-touch-icon.png,
/assets/favicon/manifest.json) so the generated site loads the CSS and icons
correctly.
Title
Add Automated Leaderboard Generation via GitHub Actions
Description
This pull request introduces a new GitHub Action that automatically creates and updates a
leaderboard.jsonfile whenever a pull request is merged into themainbranch or the workflow is manually triggered.It dynamically showcases contributors, making the project more community-driven and engaging.
Resolved issue #5885
Why This Matters
Quality Check Metadata
forge link: https://github.com/gofiber/fiber
pkg.go.dev: https://pkg.go.dev/github.com/gofiber/fiber/v2
goreportcard.com: https://goreportcard.com/report/github.com/gofiber/fiber
coverage: https://codecov.io/gh/gofiber/fiber
Summary by CodeRabbit
New Features
Documentation
Tests