mirror of
https://github.com/avelino/awesome-go.git
synced 2026-04-11 02:11:43 +08:00
Merge branch 'main' into feature/add-sixafter-nanoid
This commit is contained in:
commit
2b07e2b13a
45
.github/PULL_REQUEST_TEMPLATE.md
vendored
45
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,36 +1,47 @@
|
||||
## We want to ensure high quality of the packages. Make sure that you've checked the boxes below before sending a pull request.
|
||||
## Required links
|
||||
|
||||
_Provide the links below. Our CI will automatically validate them._
|
||||
|
||||
- [ ] Forge link (github.com, gitlab.com, etc): <!-- https://github.com/org/project -->
|
||||
- [ ] pkg.go.dev: <!-- https://pkg.go.dev/github.com/org/project -->
|
||||
- [ ] goreportcard.com: <!-- https://goreportcard.com/report/github.com/org/project -->
|
||||
- [ ] Coverage service link ([codecov](https://codecov.io/), [coveralls](https://coveralls.io/), etc.): <!-- https://app.codecov.io/gh/org/project -->
|
||||
|
||||
## Pre-submission checklist
|
||||
|
||||
- [ ] I have read the [Contribution Guidelines](https://github.com/avelino/awesome-go/blob/main/CONTRIBUTING.md#contribution-guidelines)
|
||||
- [ ] I have read the [Maintainers Note](https://github.com/avelino/awesome-go/blob/main/CONTRIBUTING.md#maintainers)
|
||||
- [ ] I have read the [Quality Standards](https://github.com/avelino/awesome-go/blob/main/CONTRIBUTING.md#quality-standards)
|
||||
|
||||
_Not every repository (project) will require every option, but most projects should. Check the Contribution Guidelines for details._
|
||||
## Repository requirements
|
||||
|
||||
_These are validated automatically by CI:_
|
||||
|
||||
- [ ] The repo has a `go.mod` file and at least one SemVer release (`vX.Y.Z`).
|
||||
- [ ] The repo has an open source license.
|
||||
- [ ] The repo documentation has a pkg.go.dev link.
|
||||
- [ ] The repo documentation has a goreportcard link (grade A- or better).
|
||||
- [ ] The repo documentation has a coverage service link.
|
||||
- [ ] The repo documentation has a goreportcard link.
|
||||
- [ ] The repo has a version-numbered release and a go.mod file.
|
||||
- [ ] The repo has a continuous integration process that automatically runs tests that must pass before new pull requests are merged.
|
||||
- [ ] Continuous integration is used to attempt to catch issues prior to releasing this package to end-users.
|
||||
|
||||
## Please provide some links to your package to ease the review
|
||||
_These are recommended and reported as warnings:_
|
||||
|
||||
- [ ] forge link (github.com, gitlab.com, etc):
|
||||
- [ ] pkg.go.dev:
|
||||
- [ ] goreportcard.com:
|
||||
- [ ] coverage service link ([codecov](https://codecov.io/), [coveralls](https://coveralls.io/), etc.):
|
||||
- [ ] The repo has a continuous integration process (GitHub Actions, etc.).
|
||||
- [ ] CI runs tests that must pass before merging.
|
||||
|
||||
## Pull Request content
|
||||
|
||||
- [ ] The package has been added to the list in alphabetical order.
|
||||
- [ ] The package has an appropriate description with correct grammar.
|
||||
- [ ] As far as I know, the package has not been listed here before.
|
||||
_These are validated automatically by CI:_
|
||||
|
||||
- [ ] This PR adds/removes/changes **only one** package.
|
||||
- [ ] The package has been added in **alphabetical order**.
|
||||
- [ ] The link text is the **exact project name**.
|
||||
- [ ] The description is clear, concise, non-promotional, and **ends with a period**.
|
||||
- [ ] The link in README.md matches the forge link above.
|
||||
|
||||
## Category quality
|
||||
|
||||
_Note that new categories can be added only when there are 3 packages or more._
|
||||
_Note: new categories require a minimum of 3 packages._
|
||||
|
||||
Packages added a long time ago might not meet the current guidelines anymore. It would be very helpful if you could check 3-5 packages above and below your submission to ensure that they also still meet the Quality Standards.
|
||||
Packages added a long time ago might not meet the current guidelines anymore. It would be very helpful if you could check 3-5 packages above and below your submission to ensure they still meet the Quality Standards.
|
||||
|
||||
Please delete one of the following lines:
|
||||
|
||||
|
||||
394
.github/scripts/check-pr-diff/main.go
vendored
Normal file
394
.github/scripts/check-pr-diff/main.go
vendored
Normal file
@ -0,0 +1,394 @@
|
||||
// check-pr-diff validates the actual changes in a PR against CONTRIBUTING.md rules:
|
||||
// - PR modifies only README.md (for package additions)
|
||||
// - PR adds or removes exactly one item
|
||||
// - Added link matches the forge link declared in PR body
|
||||
// - Link text matches the repository/project name
|
||||
// - Description ends with a period and is non-promotional
|
||||
// - Category has minimum 3 items after the change
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
reForgeLink = regexp.MustCompile(`(?i)forge\s+link[^:]*:\s*(https?://(?:github\.com|gitlab\.com|bitbucket\.org)/\S+)`)
|
||||
reEntry = regexp.MustCompile(`^- \[([^\]]+)\]\(([^)]+)\)\s+-\s+(.+)$`)
|
||||
reHeading = regexp.MustCompile(`^#{2,3}\s+(.+)`)
|
||||
)
|
||||
|
||||
// Words that indicate promotional language in descriptions.
|
||||
var promotionalWords = []string{
|
||||
"best", "fastest", "ultimate", "world-class", "blazing",
|
||||
"revolutionary", "cutting-edge", "enterprise-grade", "next-generation",
|
||||
"state-of-the-art", "unparalleled", "unmatched", "superior",
|
||||
"number one", "#1", "award-winning", "top-rated",
|
||||
}
|
||||
|
||||
type prEvent struct {
|
||||
PullRequest struct {
|
||||
Body string `json:"body"`
|
||||
} `json:"pull_request"`
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
name string
|
||||
url string
|
||||
description string
|
||||
raw string
|
||||
}
|
||||
|
||||
func main() {
|
||||
event := readEvent()
|
||||
body := event.PullRequest.Body
|
||||
forgeLink := captureMatch(body, reForgeLink)
|
||||
|
||||
var (
|
||||
results []string
|
||||
warnings []string
|
||||
hasFail bool
|
||||
)
|
||||
|
||||
// 1. Check which files were changed
|
||||
changedFiles := getChangedFiles()
|
||||
includesReadme := false
|
||||
for _, f := range changedFiles {
|
||||
if f == "README.md" {
|
||||
includesReadme = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !includesReadme {
|
||||
results = append(results, icon(false)+" **README change**: no changes to README.md detected")
|
||||
results = append(results, fix("Your PR should add or remove an entry in README.md.", "Edit README.md following the format: `- [project-name](url) - Short description.`"))
|
||||
hasFail = true
|
||||
outputResults(results, warnings, hasFail)
|
||||
return
|
||||
}
|
||||
|
||||
readmeOnly := len(changedFiles) == 1 && changedFiles[0] == "README.md"
|
||||
if readmeOnly {
|
||||
results = append(results, icon(true)+" **Files changed**: only README.md")
|
||||
} else {
|
||||
var others []string
|
||||
for _, f := range changedFiles {
|
||||
if f != "README.md" {
|
||||
others = append(others, f)
|
||||
}
|
||||
}
|
||||
warnings = append(warnings, fmt.Sprintf("%s **Extra files changed**: %s (expected only README.md for package additions)", warnIcon(false), strings.Join(others, ", ")))
|
||||
warnings = append(warnings, fix("Package addition PRs should only modify README.md.", "If you need other changes, please open a separate PR."))
|
||||
}
|
||||
|
||||
// 2. Parse the diff
|
||||
diff := getDiff()
|
||||
if diff == "" {
|
||||
results = append(results, icon(false)+" **Diff**: could not read diff for README.md")
|
||||
hasFail = true
|
||||
outputResults(results, warnings, hasFail)
|
||||
return
|
||||
}
|
||||
|
||||
added, removed := parseDiffEntries(diff)
|
||||
totalChanges := len(added) + len(removed)
|
||||
|
||||
// 3. Single item check
|
||||
switch {
|
||||
case len(added) == 1 && len(removed) == 0:
|
||||
results = append(results, icon(true)+" **Single item**: one package added")
|
||||
case len(removed) == 1 && len(added) == 0:
|
||||
results = append(results, icon(true)+" **Single item**: one package removed")
|
||||
case len(added) == 1 && len(removed) == 1:
|
||||
warnings = append(warnings, warnIcon(false)+" **Changes**: 1 added + 1 removed (update or move — please confirm in PR description)")
|
||||
case totalChanges == 0:
|
||||
warnings = append(warnings, warnIcon(false)+" **Entries**: no package entries detected in diff (might be a category or formatting change)")
|
||||
default:
|
||||
results = append(results, fmt.Sprintf("%s **Single item**: %d added, %d removed (expected exactly 1 change per PR)", icon(false), len(added), len(removed)))
|
||||
results = append(results, fix("Each PR should add, remove, or change only **one** package.", "Please split this into separate PRs — one package per PR."))
|
||||
hasFail = true
|
||||
}
|
||||
|
||||
// 4. Validate added entries
|
||||
for _, e := range added {
|
||||
// 4a. Link matches forge link in PR body
|
||||
if forgeLink != "" {
|
||||
if normalizeURL(e.url) == normalizeURL(forgeLink) {
|
||||
results = append(results, icon(true)+" **Link consistency**: README link matches forge link in PR body")
|
||||
} else {
|
||||
results = append(results, fmt.Sprintf("%s **Link consistency**: README link `%s` does not match forge link `%s`", icon(false), e.url, forgeLink))
|
||||
results = append(results, fix("The URL you added to README.md must match the forge link in your PR description.", fmt.Sprintf("Either update the README entry to use `%s`, or update `Forge link:` in your PR body to `%s`.", forgeLink, e.url)))
|
||||
hasFail = true
|
||||
}
|
||||
}
|
||||
|
||||
// 4b. Link text matches repo name
|
||||
repoName := extractRepoName(e.url)
|
||||
if repoName != "" {
|
||||
if strings.EqualFold(e.name, repoName) {
|
||||
results = append(results, icon(true)+" **Link text**: matches repository name")
|
||||
} else {
|
||||
warnings = append(warnings, fmt.Sprintf("%s **Link text**: `%s` differs from repo name `%s`", warnIcon(false), e.name, repoName))
|
||||
warnings = append(warnings, fix("The link text should be the exact project name.", fmt.Sprintf("If the project name really is `%s`, this is fine. Otherwise change it to: `- [%s](%s) - ...`", e.name, repoName, e.url)))
|
||||
}
|
||||
}
|
||||
|
||||
// 4c. Description ends with period
|
||||
if strings.HasSuffix(e.description, ".") || strings.HasSuffix(e.description, "!") {
|
||||
results = append(results, icon(true)+" **Description**: ends with punctuation")
|
||||
} else {
|
||||
results = append(results, icon(false)+" **Description**: must end with a period")
|
||||
results = append(results, fix("Add a period `.` at the end of the description.", fmt.Sprintf("Change to: `- [%s](%s) - %s.`", e.name, e.url, e.description)))
|
||||
hasFail = true
|
||||
}
|
||||
|
||||
// 4d. Non-promotional check
|
||||
descLower := strings.ToLower(e.description)
|
||||
var promoFound []string
|
||||
for _, w := range promotionalWords {
|
||||
if strings.Contains(descLower, w) {
|
||||
promoFound = append(promoFound, fmt.Sprintf("%q", w))
|
||||
}
|
||||
}
|
||||
if len(promoFound) > 0 {
|
||||
warnings = append(warnings, fmt.Sprintf("%s **Promotional language**: description contains: %s", warnIcon(false), strings.Join(promoFound, ", ")))
|
||||
warnings = append(warnings, fix("Descriptions should be factual and neutral, not promotional.", "Remove superlatives and marketing language. Good: `Lightweight HTTP router for Go.` Bad: `The fastest, best HTTP router ever.`"))
|
||||
} else {
|
||||
results = append(results, icon(true)+" **Description tone**: no promotional language detected")
|
||||
}
|
||||
|
||||
// 4e. Category minimum items
|
||||
cat, count := getCategoryItemCount("README.md", e.url)
|
||||
if count >= 3 {
|
||||
results = append(results, fmt.Sprintf("%s **Category size**: %s has %d items", icon(true), cat, count))
|
||||
} else if count > 0 {
|
||||
results = append(results, fmt.Sprintf("%s **Category size**: %s has only %d item(s) (minimum 3 required)", icon(false), cat, count))
|
||||
results = append(results, fix("Categories must have at least 3 packages.", "Either add more packages to this category in the same PR, or add your package to an existing category that already has 3+ items."))
|
||||
hasFail = true
|
||||
}
|
||||
}
|
||||
|
||||
outputResults(results, warnings, hasFail)
|
||||
}
|
||||
|
||||
// --- Event reading ---
|
||||
|
||||
func readEvent() prEvent {
|
||||
var ev prEvent
|
||||
path := os.Getenv("GITHUB_EVENT_PATH")
|
||||
if path == "" {
|
||||
return ev
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ev
|
||||
}
|
||||
_ = json.Unmarshal(data, &ev)
|
||||
return ev
|
||||
}
|
||||
|
||||
func captureMatch(s string, re *regexp.Regexp) string {
|
||||
m := re.FindStringSubmatch(s)
|
||||
if len(m) < 2 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(m[1])
|
||||
}
|
||||
|
||||
// --- Git helpers ---
|
||||
|
||||
func getDiff() string {
|
||||
base := os.Getenv("GITHUB_BASE_REF")
|
||||
if base == "" {
|
||||
base = "main"
|
||||
}
|
||||
out, err := exec.Command("git", "diff", "origin/"+base+"...HEAD", "--", "README.md").Output()
|
||||
if err == nil && len(out) > 0 {
|
||||
return string(out)
|
||||
}
|
||||
out, err = exec.Command("git", "diff", "HEAD~1", "--", "README.md").Output()
|
||||
if err == nil {
|
||||
return string(out)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getChangedFiles() []string {
|
||||
base := os.Getenv("GITHUB_BASE_REF")
|
||||
if base == "" {
|
||||
base = "main"
|
||||
}
|
||||
out, err := exec.Command("git", "diff", "--name-only", "origin/"+base+"...HEAD").Output()
|
||||
if err == nil && len(out) > 0 {
|
||||
return splitLines(string(out))
|
||||
}
|
||||
out, err = exec.Command("git", "diff", "--name-only", "HEAD~1").Output()
|
||||
if err == nil {
|
||||
return splitLines(string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitLines(s string) []string {
|
||||
var lines []string
|
||||
for _, l := range strings.Split(strings.TrimSpace(s), "\n") {
|
||||
l = strings.TrimSpace(l)
|
||||
if l != "" {
|
||||
lines = append(lines, l)
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// --- Diff parsing ---
|
||||
|
||||
func parseDiffEntries(diff string) (added, removed []entry) {
|
||||
for _, line := range strings.Split(diff, "\n") {
|
||||
if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
|
||||
content := strings.TrimSpace(line[1:])
|
||||
if e, ok := parseEntry(content); ok {
|
||||
added = append(added, e)
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") {
|
||||
content := strings.TrimSpace(line[1:])
|
||||
if e, ok := parseEntry(content); ok {
|
||||
removed = append(removed, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseEntry(line string) (entry, bool) {
|
||||
m := reEntry.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
return entry{}, false
|
||||
}
|
||||
return entry{name: m[1], url: m[2], description: m[3], raw: line}, true
|
||||
}
|
||||
|
||||
// --- URL helpers ---
|
||||
|
||||
func normalizeURL(u string) string {
|
||||
u = strings.TrimRight(u, "/")
|
||||
return strings.ToLower(u)
|
||||
}
|
||||
|
||||
func extractRepoName(rawURL string) string {
|
||||
parts := strings.Split(strings.Trim(rawURL, "/"), "/")
|
||||
if len(parts) >= 2 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// --- README parsing ---
|
||||
|
||||
func getCategoryItemCount(readmePath, entryURL string) (category string, count int) {
|
||||
data, err := os.ReadFile(readmePath)
|
||||
if err != nil {
|
||||
return "unknown", -1
|
||||
}
|
||||
|
||||
var currentCat string
|
||||
var catItems int
|
||||
var foundCat string
|
||||
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if m := reHeading.FindStringSubmatch(trimmed); m != nil {
|
||||
if foundCat != "" {
|
||||
break // passed the target category
|
||||
}
|
||||
currentCat = m[1]
|
||||
catItems = 0
|
||||
}
|
||||
if reEntry.MatchString(trimmed) {
|
||||
catItems++
|
||||
if strings.Contains(trimmed, entryURL) {
|
||||
foundCat = currentCat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if foundCat == "" {
|
||||
return "unknown", -1
|
||||
}
|
||||
return foundCat, catItems
|
||||
}
|
||||
|
||||
// --- Output ---
|
||||
|
||||
func outputResults(results, warnings []string, hasFail bool) {
|
||||
var lines []string
|
||||
lines = append(lines, "## PR Diff Validation", "")
|
||||
|
||||
if len(results) > 0 {
|
||||
lines = append(lines, "### Content checks", "")
|
||||
lines = append(lines, results...)
|
||||
lines = append(lines, "")
|
||||
}
|
||||
|
||||
if len(warnings) > 0 {
|
||||
lines = append(lines, "### Warnings", "")
|
||||
lines = append(lines, warnings...)
|
||||
lines = append(lines, "")
|
||||
}
|
||||
|
||||
if hasFail {
|
||||
lines = append(lines, "---")
|
||||
lines = append(lines, "> **Action needed:** one or more content checks failed. Please review the [contribution guidelines](https://github.com/avelino/awesome-go/blob/main/CONTRIBUTING.md).")
|
||||
}
|
||||
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "_Automated diff validation — does not replace maintainer review._")
|
||||
|
||||
comment := strings.Join(lines, "\n")
|
||||
setOutput("diff_comment", comment)
|
||||
setOutput("diff_fail", boolStr(hasFail))
|
||||
}
|
||||
|
||||
func setOutput(name, value string) {
|
||||
path := os.Getenv("GITHUB_OUTPUT")
|
||||
if path == "" {
|
||||
fmt.Printf("%s=%s\n", name, value)
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
fmt.Fprintf(f, "%s<<EOF\n%s\nEOF\n", name, value)
|
||||
}
|
||||
|
||||
func icon(ok bool) string {
|
||||
if ok {
|
||||
return "\u2705"
|
||||
}
|
||||
return "\u274C"
|
||||
}
|
||||
|
||||
func warnIcon(ok bool) string {
|
||||
if ok {
|
||||
return "\u2705"
|
||||
}
|
||||
return "\u26A0\uFE0F"
|
||||
}
|
||||
|
||||
func boolStr(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func fix(problem, howToFix string) string {
|
||||
return fmt.Sprintf(" > **How to fix:** %s\n > %s", problem, howToFix)
|
||||
}
|
||||
330
.github/scripts/check-quality.js
vendored
330
.github/scripts/check-quality.js
vendored
@ -1,330 +0,0 @@
|
||||
/*
|
||||
PR Quality Checks `CONTRIBUTING.md`
|
||||
- Extracts links from PR body (repo, pkg.go.dev, goreportcard, coverage)
|
||||
- Validates minimum standards from CONTRIBUTING.md (basic automated subset):
|
||||
* Repo accessible and not archived
|
||||
* Has go.mod and at least one SemVer release
|
||||
* Go Report Card reachable with acceptable grade (A- or better)
|
||||
* Coverage link reachable
|
||||
* pkg.go.dev page exists
|
||||
- Outputs a markdown report as `comment` and sets `fail=true` if critical checks fail
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
|
||||
const GITHUB_OUTPUT = process.env.GITHUB_OUTPUT;
|
||||
|
||||
/**
|
||||
* Read and parse the GitHub event JSON pointed to by GITHUB_EVENT_PATH.
|
||||
*
|
||||
* Attempts to read the file at the path given by the environment variable
|
||||
* `GITHUB_EVENT_PATH` and return its parsed JSON object. If the file is
|
||||
* missing, unreadable, or contains invalid JSON, returns an empty object.
|
||||
*
|
||||
* @returns {Object} The parsed event payload, or an empty object on error.
|
||||
*/
|
||||
function readEvent() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the first capture group from `body` using `regex` and returns it trimmed.
|
||||
*
|
||||
* @param {string} body - Input text to match against.
|
||||
* @param {RegExp} regex - Regular expression that contains at least one capture group; the first group's value is returned.
|
||||
* @return {string} The trimmed first capture group if matched, otherwise an empty string.
|
||||
*/
|
||||
function capture(body, regex) {
|
||||
const m = body.match(regex);
|
||||
return m && m[1] ? m[1].trim() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a URL is reachable using an HTTP HEAD request with a GET fallback.
|
||||
*
|
||||
* Performs a HEAD request to the provided URL and:
|
||||
* - resolves { ok: true, status } for 2xx–3xx responses,
|
||||
* - resolves { ok: false, status } for 4xx responses,
|
||||
* - for other responses (including redirects or servers that don't support HEAD) retries with GET and resolves { ok: boolean, status } based on the GET status.
|
||||
* Network or request errors resolve to { ok: false }.
|
||||
*
|
||||
* The returned Promise never rejects; it always resolves with an object describing reachability.
|
||||
*
|
||||
* @param {string} url - The URL to check.
|
||||
* @returns {Promise<{ok: boolean, status?: number}>} Reachability result; `status` is the HTTP status code when available.
|
||||
*/
|
||||
function httpHeadOrGet(url) {
|
||||
return new Promise((resolve) => {
|
||||
const req = https.request(url, { method: 'HEAD' }, (res) => {
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 400) {
|
||||
resolve({ ok: true, status: res.statusCode });
|
||||
} else if (res.statusCode && res.statusCode >= 400 && res.statusCode < 500) {
|
||||
resolve({ ok: false, status: res.statusCode });
|
||||
} else {
|
||||
// retry with GET on redirect or unsupported HEAD
|
||||
const req2 = https.request(url, { method: 'GET' }, (res2) => {
|
||||
resolve({ ok: (res2.statusCode || 500) < 400, status: res2.statusCode });
|
||||
});
|
||||
req2.on('error', () => resolve({ ok: false }));
|
||||
req2.end();
|
||||
return;
|
||||
}
|
||||
});
|
||||
req.on('error', () => resolve({ ok: false }));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a GitHub repository URL and return its owner and repository name.
|
||||
* @param {string} repoUrl - URL pointing to a GitHub repository (e.g. "https://github.com/owner/repo").
|
||||
* @returns {{owner: string, repo: string}|null} An object with `owner` and `repo` when the URL is a valid GitHub repository path; otherwise `null`.
|
||||
*/
|
||||
function parseGithubRepo(repoUrl) {
|
||||
try {
|
||||
const u = new URL(repoUrl);
|
||||
if (u.hostname !== 'github.com') return null;
|
||||
const [, owner, repo] = u.pathname.split('/');
|
||||
if (!owner || !repo) return null;
|
||||
return { owner, repo };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch JSON from the given URL via HTTPS GET and return the parsed object.
|
||||
*
|
||||
* Performs an HTTPS GET request to the provided URL and returns the parsed
|
||||
* JSON body. If the response body is not valid JSON or a network error
|
||||
* occurs, resolves to null.
|
||||
*
|
||||
* @param {string} url - The URL to fetch.
|
||||
* @param {Object} [headers={}] - Optional HTTP headers to include in the request.
|
||||
* @returns {Promise<Object|null>} The parsed JSON object, or null on error or invalid JSON.
|
||||
*/
|
||||
async function fetchJson(url, headers = {}) {
|
||||
return new Promise((resolve) => {
|
||||
https
|
||||
.get(url, { headers }, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (c) => (data += c));
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
})
|
||||
.on('error', () => resolve(null));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a GitHub repository URL for Go project quality requirements.
|
||||
*
|
||||
* Performs API checks to ensure the repository exists, is not archived,
|
||||
* contains a top-level `go.mod` file, and has at least one SemVer-formatted
|
||||
* release (tag matching `^v\d+\.\d+\.\d+`).
|
||||
*
|
||||
* @param {string} repoUrl - A repository URL (expected to be a GitHub URL).
|
||||
* @returns {Promise<{ok: boolean, reason?: string}>} Resolves with an object where
|
||||
* `ok` is true only if the repo is reachable, not archived, has `go.mod`, and
|
||||
* has a SemVer release. When `ok` is false, `reason` is one of:
|
||||
* - "invalid repo url"
|
||||
* - "repo api not reachable"
|
||||
* - "repo is archived"
|
||||
* - "missing go.mod"
|
||||
* - "missing semver release"
|
||||
*/
|
||||
async function checkGithubRepo(repoUrl) {
|
||||
const parsed = parseGithubRepo(repoUrl);
|
||||
if (!parsed) return { ok: false, reason: 'invalid repo url' };
|
||||
const { owner, repo } = parsed;
|
||||
const base = 'https://api.github.com';
|
||||
const headers = {
|
||||
'User-Agent': 'awesome-go-quality-check',
|
||||
'Accept': 'application/vnd.github+json',
|
||||
};
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
const repoData = await fetchJson(`${base}/repos/${owner}/${repo}`, headers);
|
||||
if (!repoData) return { ok: false, reason: 'repo api not reachable' };
|
||||
if (repoData.archived) return { ok: false, reason: 'repo is archived' };
|
||||
const hasGoMod = await fetchJson(`${base}/repos/${owner}/${repo}/contents/go.mod`, headers);
|
||||
const releases = await fetchJson(`${base}/repos/${owner}/${repo}/releases`, headers);
|
||||
const hasRelease = Array.isArray(releases) && releases.some((r) => /^v\d+\.\d+\.\d+/.test(r.tag_name || ''));
|
||||
const hasGoModOk = Boolean(hasGoMod && hasGoMod.name === 'go.mod');
|
||||
return {
|
||||
ok: Boolean(hasGoModOk && hasRelease),
|
||||
reason: !hasGoModOk ? 'missing go.mod' : !hasRelease ? 'missing semver release' : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a Go Report Card page for reachability and grade (pass if A- or better).
|
||||
*
|
||||
* Performs a HEAD/GET reachability check; if reachable, fetches the page and
|
||||
* attempts to parse a `Grade: X` token (case-insensitive). Returns:
|
||||
* - { ok: false, reason: 'unreachable' } if the initial reachability check fails.
|
||||
* - { ok: false, reason: 'fetch error' } if the page fetch errors.
|
||||
* - { ok: true, grade: 'unknown' } if the page is reachable but no grade is found.
|
||||
* - { ok: true, grade: 'A'|'A+'|'A-'|... } for parsed grades; `ok` is true only
|
||||
* when the parsed grade is A, A+ or A- (passes), otherwise `ok` is false.
|
||||
*
|
||||
* @param {string} url - URL of the Go Report Card page to check.
|
||||
* @return {Promise<Object>} Result object containing `ok` and either `grade` or `reason`.
|
||||
*/
|
||||
async function checkGoReportCard(url) {
|
||||
// Accept A- or better
|
||||
const res = await httpHeadOrGet(url);
|
||||
if (!res.ok) return { ok: false, reason: 'unreachable' };
|
||||
// Fetch page to parse grade (best-effort)
|
||||
return new Promise((resolve) => {
|
||||
https
|
||||
.get(url, (res2) => {
|
||||
let html = '';
|
||||
res2.on('data', (c) => (html += c));
|
||||
res2.on('end', () => {
|
||||
const m = html.match(/Grade:\s*([A-F][+-]?)/i);
|
||||
if (!m) return resolve({ ok: true, grade: 'unknown' });
|
||||
const grade = m[1].toUpperCase();
|
||||
const pass = /^A[-+]?$/.test(grade);
|
||||
resolve({ ok: pass, grade });
|
||||
});
|
||||
})
|
||||
.on('error', () => resolve({ ok: false, reason: 'fetch error' }));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a pkg.go.dev page is reachable.
|
||||
*
|
||||
* Performs an HTTP HEAD (with GET fallback) to the provided URL and returns an object indicating reachability.
|
||||
*
|
||||
* @param {string} url - The pkg.go.dev URL to check.
|
||||
* @return {{ok: boolean}} An object with `ok: true` when the page is reachable, otherwise `{ok: false}`.
|
||||
*/
|
||||
async function checkPkgGoDev(url) {
|
||||
const res = await httpHeadOrGet(url);
|
||||
return { ok: res.ok };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a coverage service URL is reachable.
|
||||
*
|
||||
* @param {string} url - The coverage page URL to validate (e.g., Coveralls or Codecov link).
|
||||
* @returns {{ok: boolean}} An object with `ok: true` if the URL responded successfully, otherwise `ok: false`.
|
||||
*/
|
||||
async function checkCoverage(url) {
|
||||
const res = await httpHeadOrGet(url);
|
||||
return { ok: res.ok };
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a named workflow output to the GitHub Actions GITHUB_OUTPUT file using a heredoc block.
|
||||
*
|
||||
* If the GITHUB_OUTPUT environment variable is unset, this is a no-op.
|
||||
*
|
||||
* @param {string} name - The output name (key) to write.
|
||||
* @param {string} value - The output value; written between the heredoc delimiters.
|
||||
*/
|
||||
function setOutput(name, value) {
|
||||
if (!GITHUB_OUTPUT) return;
|
||||
fs.appendFileSync(GITHUB_OUTPUT, `${name}<<EOF\n${value}\nEOF\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run automated PR quality checks and emit GitHub Actions outputs.
|
||||
*
|
||||
* Reads the current PR body, extracts links for the repository (forge), pkg.go.dev,
|
||||
* goreportcard, and coverage services, performs best-effort validations for each,
|
||||
* and emits three GitHub Actions outputs: `comment` (markdown summary), `fail`
|
||||
* ("true"/"false"), and `labels` (JSON array). The comment summarizes missing links
|
||||
* and the result of each check; labels reflect missing information, coverage status,
|
||||
* and overall quality pass/fail.
|
||||
*/
|
||||
async function main() {
|
||||
const event = readEvent();
|
||||
const body = (event.pull_request && event.pull_request.body) || '';
|
||||
const repo = capture(body, /forge\s+link[^:]*:\s*(https?:\/\/(?:github\.com|gitlab\.com|bitbucket\.org)\/\S+)/i);
|
||||
const pkg = capture(body, /pkg\.go\.dev:\s*(https?:\/\/pkg\.go\.dev\/\S+)/i);
|
||||
const gorep = capture(body, /goreportcard\.com:\s*(https?:\/\/goreportcard\.com\/\S+)/i);
|
||||
const coverage = capture(body, /coverage[^:]*:\s*(https?:\/\/(?:coveralls\.io|codecov\.io)\/\S+)/i);
|
||||
|
||||
const results = [];
|
||||
let criticalFail = false;
|
||||
let repoOk = false, pkgOk = false, gorepOk = false; // track core checks
|
||||
|
||||
if (!repo) {
|
||||
results.push('- Repo link: missing');
|
||||
criticalFail = true;
|
||||
} else {
|
||||
const r = await checkGithubRepo(repo);
|
||||
if (!r.ok) { results.push(`- Repo: FAIL (${r.reason || 'unknown'})`); criticalFail = true; }
|
||||
else { results.push('- Repo: OK'); repoOk = true; }
|
||||
}
|
||||
|
||||
if (!pkg) {
|
||||
results.push('- pkg.go.dev: missing');
|
||||
criticalFail = true;
|
||||
} else {
|
||||
const r = await checkPkgGoDev(pkg);
|
||||
if (!r.ok) { results.push('- pkg.go.dev: FAIL (unreachable)'); criticalFail = true; }
|
||||
else { results.push('- pkg.go.dev: OK'); pkgOk = true; }
|
||||
}
|
||||
|
||||
if (!gorep) {
|
||||
results.push('- goreportcard: missing');
|
||||
criticalFail = true;
|
||||
} else {
|
||||
const r = await checkGoReportCard(gorep);
|
||||
if (!r.ok) { results.push(`- goreportcard: FAIL (${r.reason || r.grade || 'unreachable/low grade'})`); criticalFail = true; }
|
||||
else { results.push(`- goreportcard: OK${r.grade ? ` (grade ${r.grade})` : ''}`); gorepOk = true; }
|
||||
}
|
||||
|
||||
let coverageOk = false;
|
||||
if (!coverage) {
|
||||
results.push('- coverage: missing');
|
||||
} else {
|
||||
const r = await checkCoverage(coverage);
|
||||
if (!r.ok) { results.push('- coverage: FAIL (unreachable)'); }
|
||||
else { results.push('- coverage: OK'); coverageOk = true; }
|
||||
}
|
||||
|
||||
const header = 'Automated Quality Checks (from CONTRIBUTING minimum standards)';
|
||||
const note = 'These checks are a best-effort automation and do not replace human review.';
|
||||
const summary = results.join('\n');
|
||||
const comment = [header, '', summary, '', note].join('\n');
|
||||
|
||||
// Labels to reflect status
|
||||
const labels = [];
|
||||
const missingInfo = (!repo || !pkg || !gorep);
|
||||
if (missingInfo) labels.push('needs-info');
|
||||
if (!coverageOk) labels.push('needs-coverage');
|
||||
if (criticalFail) labels.push('quality:fail');
|
||||
if (!criticalFail && repoOk && pkgOk && gorepOk) labels.push('quality:ok');
|
||||
|
||||
setOutput('comment', comment);
|
||||
setOutput('fail', criticalFail ? 'true' : 'false');
|
||||
setOutput('labels', JSON.stringify(labels));
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
const msg = `Quality checks failed to run: ${e?.message || e}`;
|
||||
if (GITHUB_OUTPUT) {
|
||||
fs.appendFileSync(GITHUB_OUTPUT, `comment<<EOF\n${msg}\nEOF\n`);
|
||||
fs.appendFileSync(GITHUB_OUTPUT, `fail<<EOF\ntrue\nEOF\n`);
|
||||
}
|
||||
process.exitCode = 0; // Do not crash the job here; let the fail step handle it
|
||||
});
|
||||
|
||||
|
||||
503
.github/scripts/check-quality/main.go
vendored
Normal file
503
.github/scripts/check-quality/main.go
vendored
Normal file
@ -0,0 +1,503 @@
|
||||
// check-quality validates PR body links and repository quality against
|
||||
// CONTRIBUTING.md minimum standards. It outputs a markdown report and
|
||||
// labels via GITHUB_OUTPUT for the PR Quality Checks workflow.
|
||||
//
|
||||
// Checks performed:
|
||||
// - Repo accessible, not archived, has go.mod and SemVer release
|
||||
// - Open source license present
|
||||
// - Repository age >= 5 months
|
||||
// - CI/CD (GitHub Actions) configured
|
||||
// - README exists
|
||||
// - Go Report Card grade A- or better
|
||||
// - pkg.go.dev page reachable
|
||||
// - Coverage link reachable
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
reForgeLink = regexp.MustCompile(`(?i)forge\s+link[^:]*:\s*(https?://(?:github\.com|gitlab\.com|bitbucket\.org)/\S+)`)
|
||||
rePkgGoDev = regexp.MustCompile(`(?i)pkg\.go\.dev:\s*(https?://pkg\.go\.dev/\S+)`)
|
||||
reGoReport = regexp.MustCompile(`(?i)goreportcard\.com:\s*(https?://goreportcard\.com/\S+)`)
|
||||
reCoverage = regexp.MustCompile(`(?i)coverage[^:]*:\s*(https?://(?:coveralls\.io|(?:app\.)?codecov\.io)/\S+)`)
|
||||
reGrade = regexp.MustCompile(`(?i)Grade:\s*([A-F][+-]?)`)
|
||||
reGithubRepo = regexp.MustCompile(`^https?://github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$`)
|
||||
reSemver = regexp.MustCompile(`^v\d+\.\d+\.\d+`)
|
||||
)
|
||||
|
||||
type prEvent struct {
|
||||
PullRequest struct {
|
||||
Body string `json:"body"`
|
||||
} `json:"pull_request"`
|
||||
}
|
||||
|
||||
type repoData struct {
|
||||
Archived bool `json:"archived"`
|
||||
License *struct {
|
||||
SPDXID string `json:"spdx_id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"license"`
|
||||
Message string `json:"message"` // present on API errors
|
||||
}
|
||||
|
||||
type contentEntry struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type tagEntry struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type workflowsResponse struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
event := readEvent()
|
||||
body := event.PullRequest.Body
|
||||
|
||||
forgeLink := captureMatch(body, reForgeLink)
|
||||
pkgLink := captureMatch(body, rePkgGoDev)
|
||||
gorepLink := captureMatch(body, reGoReport)
|
||||
covLink := captureMatch(body, reCoverage)
|
||||
|
||||
var (
|
||||
critical []string
|
||||
warnings []string
|
||||
criticalFail bool
|
||||
repoOk bool
|
||||
pkgOk bool
|
||||
gorepOk bool
|
||||
coverageOk bool
|
||||
labels []string
|
||||
)
|
||||
|
||||
// --- Repo checks ---
|
||||
if forgeLink == "" {
|
||||
critical = append(critical, icon(false)+" **Repo link**: missing from PR body")
|
||||
critical = append(critical, fix("Add the following to your PR description:", "```\nForge link: https://github.com/your-org/your-project\n```"))
|
||||
criticalFail = true
|
||||
} else {
|
||||
r := checkGithubRepo(forgeLink)
|
||||
if !r.ok {
|
||||
critical = append(critical, fmt.Sprintf("%s **Repo**: %s", icon(false), r.reason))
|
||||
critical = append(critical, repoFixMessage(r.reason, forgeLink))
|
||||
criticalFail = true
|
||||
} else {
|
||||
critical = append(critical, icon(true)+" **Repo**: accessible, has go.mod and SemVer release")
|
||||
repoOk = true
|
||||
}
|
||||
|
||||
if r.licenseChecked {
|
||||
if r.hasLicense {
|
||||
warnings = append(warnings, fmt.Sprintf("%s **License**: %s", warnIcon(true), r.licenseName))
|
||||
} else {
|
||||
warnings = append(warnings, warnIcon(false)+" **License**: no open source license detected")
|
||||
warnings = append(warnings, fix("Add a LICENSE file to your repository root.", "Choose one at https://choosealicense.com — common choices for Go projects: MIT, Apache-2.0, BSD-3-Clause."))
|
||||
labels = append(labels, "needs-license")
|
||||
}
|
||||
}
|
||||
if r.maturityChecked {
|
||||
if r.hasFiveMonths {
|
||||
warnings = append(warnings, warnIcon(true)+" **Maturity**: repo has 5+ months of history")
|
||||
} else {
|
||||
warnings = append(warnings, warnIcon(false)+" **Maturity**: repo appears to have less than 5 months of history")
|
||||
warnings = append(warnings, fix("Your repository needs at least 5 months of history since the first commit.", "Please resubmit after the repository meets this requirement."))
|
||||
labels = append(labels, "needs-maturity")
|
||||
}
|
||||
}
|
||||
if r.ciChecked {
|
||||
if r.hasCI {
|
||||
warnings = append(warnings, warnIcon(true)+" **CI/CD**: GitHub Actions workflows detected")
|
||||
} else {
|
||||
warnings = append(warnings, warnIcon(false)+" **CI/CD**: no GitHub Actions workflows found")
|
||||
warnings = append(warnings, fix("Add a CI workflow to run tests automatically.", "Create `.github/workflows/test.yml` — see https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-go"))
|
||||
}
|
||||
}
|
||||
if r.readmeChecked {
|
||||
if r.hasReadme {
|
||||
warnings = append(warnings, warnIcon(true)+" **README**: present")
|
||||
} else {
|
||||
warnings = append(warnings, warnIcon(false)+" **README**: not found in repository root")
|
||||
warnings = append(warnings, fix("Add a `README.md` to your repository root.", "It should explain what the project does, how to install it, and how to use it, in English."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- pkg.go.dev ---
|
||||
if pkgLink == "" {
|
||||
critical = append(critical, icon(false)+" **pkg.go.dev**: missing from PR body")
|
||||
critical = append(critical, fix("Add the following to your PR description:", "```\npkg.go.dev: https://pkg.go.dev/github.com/your-org/your-project\n```"))
|
||||
criticalFail = true
|
||||
} else if !isReachable(pkgLink) {
|
||||
critical = append(critical, icon(false)+" **pkg.go.dev**: unreachable")
|
||||
critical = append(critical, fix("The pkg.go.dev page could not be reached.", "Ensure your module path in `go.mod` matches the URL. After pushing a tagged release, pkg.go.dev indexes the module automatically — this can take a few minutes. You can trigger it manually by visiting `https://pkg.go.dev/your-module-path`."))
|
||||
criticalFail = true
|
||||
} else {
|
||||
critical = append(critical, icon(true)+" **pkg.go.dev**: OK")
|
||||
pkgOk = true
|
||||
}
|
||||
|
||||
// --- Go Report Card ---
|
||||
if gorepLink == "" {
|
||||
critical = append(critical, icon(false)+" **Go Report Card**: missing from PR body")
|
||||
if forgeLink != "" {
|
||||
critical = append(critical, fix("Add the following to your PR description:", fmt.Sprintf("```\ngoreportcard.com: https://goreportcard.com/report/%s\n```", strings.TrimPrefix(strings.TrimPrefix(forgeLink, "https://"), "http://"))))
|
||||
} else {
|
||||
critical = append(critical, fix("Add the following to your PR description:", "```\ngoreportcard.com: https://goreportcard.com/report/github.com/your-org/your-project\n```"))
|
||||
}
|
||||
criticalFail = true
|
||||
} else {
|
||||
grade, ok := checkGoReportCard(gorepLink)
|
||||
if !ok {
|
||||
critical = append(critical, fmt.Sprintf("%s **Go Report Card**: %s", icon(false), grade))
|
||||
if grade == "unreachable" || grade == "fetch error" {
|
||||
critical = append(critical, fix("The Go Report Card page could not be reached.", "Visit https://goreportcard.com and generate a report for your project. Then add the correct link to your PR body."))
|
||||
} else {
|
||||
critical = append(critical, fix(fmt.Sprintf("Your project received grade **%s** — minimum required is **A-**.", grade), "Run `gofmt -s -w .` to fix formatting, `go vet ./...` to fix vet issues, and review the report at "+gorepLink+" for specific problems to address."))
|
||||
}
|
||||
criticalFail = true
|
||||
} else {
|
||||
msg := icon(true) + " **Go Report Card**: OK"
|
||||
if grade != "" {
|
||||
msg += fmt.Sprintf(" (grade %s)", grade)
|
||||
}
|
||||
critical = append(critical, msg)
|
||||
gorepOk = true
|
||||
}
|
||||
}
|
||||
|
||||
// --- Coverage ---
|
||||
if covLink == "" {
|
||||
warnings = append(warnings, warnIcon(false)+" **Coverage**: missing from PR body")
|
||||
warnings = append(warnings, fix("Add a coverage service link to your PR description:", "```\nCoverage: https://app.codecov.io/gh/your-org/your-project\n```\nPopular options: [Codecov](https://codecov.io), [Coveralls](https://coveralls.io). Integrate one with your CI to track coverage automatically."))
|
||||
} else if !isReachable(covLink) {
|
||||
warnings = append(warnings, warnIcon(false)+" **Coverage**: unreachable")
|
||||
warnings = append(warnings, fix("The coverage link could not be reached.", "Ensure the coverage service is configured for your repository and the link is correct. If you just set it up, it may need a CI run to generate the first report."))
|
||||
} else {
|
||||
warnings = append(warnings, warnIcon(true)+" **Coverage**: link accessible")
|
||||
coverageOk = true
|
||||
}
|
||||
|
||||
// --- Build comment ---
|
||||
var lines []string
|
||||
lines = append(lines, "## Automated Quality Checks", "")
|
||||
lines = append(lines, "### Required checks", "")
|
||||
lines = append(lines, critical...)
|
||||
lines = append(lines, "", "### Additional checks", "")
|
||||
lines = append(lines, warnings...)
|
||||
lines = append(lines, "")
|
||||
|
||||
if criticalFail {
|
||||
lines = append(lines, "---")
|
||||
lines = append(lines, "> **Action needed:** one or more required checks failed. Please update your PR body with the missing links and ensure the repository meets the [quality standards](https://github.com/avelino/awesome-go/blob/main/CONTRIBUTING.md#quality-standards).")
|
||||
}
|
||||
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "_These checks are automated and do not replace maintainer review. See [CONTRIBUTING.md](https://github.com/avelino/awesome-go/blob/main/CONTRIBUTING.md) for full guidelines._")
|
||||
|
||||
comment := strings.Join(lines, "\n")
|
||||
|
||||
// --- Labels ---
|
||||
if forgeLink == "" || pkgLink == "" || gorepLink == "" {
|
||||
labels = append(labels, "needs-info")
|
||||
}
|
||||
if !coverageOk {
|
||||
labels = append(labels, "needs-coverage")
|
||||
}
|
||||
if criticalFail {
|
||||
labels = append(labels, "quality:fail")
|
||||
}
|
||||
if !criticalFail && repoOk && pkgOk && gorepOk {
|
||||
labels = append(labels, "quality:ok")
|
||||
}
|
||||
|
||||
labelsJSON, _ := json.Marshal(labels)
|
||||
|
||||
setOutput("comment", comment)
|
||||
setOutput("fail", boolStr(criticalFail))
|
||||
setOutput("labels", string(labelsJSON))
|
||||
}
|
||||
|
||||
// --- Event reading ---
|
||||
|
||||
func readEvent() prEvent {
|
||||
var ev prEvent
|
||||
path := os.Getenv("GITHUB_EVENT_PATH")
|
||||
if path == "" {
|
||||
return ev
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ev
|
||||
}
|
||||
_ = json.Unmarshal(data, &ev)
|
||||
return ev
|
||||
}
|
||||
|
||||
// --- Regex helpers ---
|
||||
|
||||
func captureMatch(s string, re *regexp.Regexp) string {
|
||||
m := re.FindStringSubmatch(s)
|
||||
if len(m) < 2 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(m[1])
|
||||
}
|
||||
|
||||
// --- HTTP helpers ---
|
||||
|
||||
var httpClient = &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
func isReachable(url string) bool {
|
||||
req, err := http.NewRequest(http.MethodHead, url, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
|
||||
return true
|
||||
}
|
||||
// Fallback to GET
|
||||
req2, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp2, err := httpClient.Do(req2)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
resp2.Body.Close()
|
||||
return resp2.StatusCode >= 200 && resp2.StatusCode < 400
|
||||
}
|
||||
|
||||
func githubGet(url string) ([]byte, int, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "awesome-go-quality-check")
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
return body, resp.StatusCode, err
|
||||
}
|
||||
|
||||
// --- GitHub repo checks ---
|
||||
|
||||
type repoCheckResult struct {
|
||||
ok bool
|
||||
reason string
|
||||
hasLicense bool
|
||||
licenseName string
|
||||
licenseChecked bool
|
||||
hasFiveMonths bool
|
||||
maturityChecked bool
|
||||
hasCI bool
|
||||
ciChecked bool
|
||||
hasReadme bool
|
||||
readmeChecked bool
|
||||
}
|
||||
|
||||
func checkGithubRepo(repoURL string) repoCheckResult {
|
||||
m := reGithubRepo.FindStringSubmatch(repoURL)
|
||||
if m == nil {
|
||||
return repoCheckResult{reason: "invalid repo url"}
|
||||
}
|
||||
owner, repo := m[1], m[2]
|
||||
base := "https://api.github.com"
|
||||
|
||||
// Fetch repo metadata
|
||||
data, status, err := githubGet(fmt.Sprintf("%s/repos/%s/%s", base, owner, repo))
|
||||
if err != nil || status >= 400 {
|
||||
return repoCheckResult{reason: "repo api not reachable"}
|
||||
}
|
||||
|
||||
var rd repoData
|
||||
if json.Unmarshal(data, &rd) != nil {
|
||||
return repoCheckResult{reason: "repo api not reachable"}
|
||||
}
|
||||
if rd.Message != "" {
|
||||
return repoCheckResult{reason: "repo api not reachable"}
|
||||
}
|
||||
if rd.Archived {
|
||||
return repoCheckResult{reason: "repo is archived"}
|
||||
}
|
||||
|
||||
result := repoCheckResult{}
|
||||
|
||||
// License
|
||||
result.licenseChecked = true
|
||||
if rd.License != nil && rd.License.SPDXID != "" && rd.License.SPDXID != "NOASSERTION" {
|
||||
result.hasLicense = true
|
||||
result.licenseName = rd.License.SPDXID
|
||||
}
|
||||
|
||||
// go.mod
|
||||
hasGoMod := false
|
||||
goModData, goModStatus, _ := githubGet(fmt.Sprintf("%s/repos/%s/%s/contents/go.mod", base, owner, repo))
|
||||
if goModStatus == 200 {
|
||||
var entry contentEntry
|
||||
if json.Unmarshal(goModData, &entry) == nil && entry.Name == "go.mod" {
|
||||
hasGoMod = true
|
||||
}
|
||||
}
|
||||
|
||||
// SemVer tags
|
||||
hasSemver := false
|
||||
tagsData, tagsStatus, _ := githubGet(fmt.Sprintf("%s/repos/%s/%s/tags?per_page=100", base, owner, repo))
|
||||
if tagsStatus == 200 {
|
||||
var tags []tagEntry
|
||||
if json.Unmarshal(tagsData, &tags) == nil {
|
||||
for _, t := range tags {
|
||||
if reSemver.MatchString(t.Name) {
|
||||
hasSemver = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Maturity (5+ months)
|
||||
result.maturityChecked = true
|
||||
fiveMonthsAgo := time.Now().AddDate(0, -5, 0).Format(time.RFC3339)
|
||||
commitsData, commitsStatus, _ := githubGet(fmt.Sprintf("%s/repos/%s/%s/commits?per_page=1&until=%s", base, owner, repo, fiveMonthsAgo))
|
||||
if commitsStatus == 200 {
|
||||
var commits []json.RawMessage
|
||||
if json.Unmarshal(commitsData, &commits) == nil && len(commits) > 0 {
|
||||
result.hasFiveMonths = true
|
||||
}
|
||||
}
|
||||
|
||||
// CI/CD (GitHub Actions)
|
||||
result.ciChecked = true
|
||||
wfData, wfStatus, _ := githubGet(fmt.Sprintf("%s/repos/%s/%s/actions/workflows", base, owner, repo))
|
||||
if wfStatus == 200 {
|
||||
var wf workflowsResponse
|
||||
if json.Unmarshal(wfData, &wf) == nil && wf.TotalCount > 0 {
|
||||
result.hasCI = true
|
||||
}
|
||||
}
|
||||
|
||||
// README
|
||||
result.readmeChecked = true
|
||||
readmeData, readmeStatus, _ := githubGet(fmt.Sprintf("%s/repos/%s/%s/readme", base, owner, repo))
|
||||
if readmeStatus == 200 {
|
||||
var entry contentEntry
|
||||
if json.Unmarshal(readmeData, &entry) == nil && entry.Name != "" {
|
||||
result.hasReadme = true
|
||||
}
|
||||
}
|
||||
|
||||
result.ok = hasGoMod && hasSemver
|
||||
if !hasGoMod {
|
||||
result.reason = "missing go.mod"
|
||||
} else if !hasSemver {
|
||||
result.reason = "missing semver release"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// --- Go Report Card ---
|
||||
|
||||
func checkGoReportCard(url string) (grade string, ok bool) {
|
||||
if !isReachable(url) {
|
||||
return "unreachable", false
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "fetch error", false
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "fetch error", false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "fetch error", false
|
||||
}
|
||||
m := reGrade.FindSubmatch(body)
|
||||
if m == nil {
|
||||
return "unknown", true // reachable but no grade found
|
||||
}
|
||||
g := strings.ToUpper(string(m[1]))
|
||||
pass := g == "A" || g == "A+" || g == "A-"
|
||||
return g, pass
|
||||
}
|
||||
|
||||
// --- Output helpers ---
|
||||
|
||||
func setOutput(name, value string) {
|
||||
path := os.Getenv("GITHUB_OUTPUT")
|
||||
if path == "" {
|
||||
fmt.Printf("%s=%s\n", name, value)
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
fmt.Fprintf(f, "%s<<EOF\n%s\nEOF\n", name, value)
|
||||
}
|
||||
|
||||
func icon(ok bool) string {
|
||||
if ok {
|
||||
return "\u2705"
|
||||
}
|
||||
return "\u274C"
|
||||
}
|
||||
|
||||
func warnIcon(ok bool) string {
|
||||
if ok {
|
||||
return "\u2705"
|
||||
}
|
||||
return "\u26A0\uFE0F"
|
||||
}
|
||||
|
||||
func boolStr(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func fix(problem, howToFix string) string {
|
||||
return fmt.Sprintf(" > **How to fix:** %s\n > %s", problem, howToFix)
|
||||
}
|
||||
|
||||
func repoFixMessage(reason, repoURL string) string {
|
||||
switch reason {
|
||||
case "invalid repo url":
|
||||
return fix("The forge link is not a valid repository URL.", "Use the full URL, e.g. `https://github.com/org/project`.")
|
||||
case "repo api not reachable":
|
||||
return fix("Could not reach the repository via GitHub API.", "Ensure the repository is **public** and the URL is correct.")
|
||||
case "repo is archived":
|
||||
return fix("This repository is archived on GitHub.", "Archived repositories are not accepted. The project must be actively maintained or at least open to contributions.")
|
||||
case "missing go.mod":
|
||||
return fix("No `go.mod` file found at the repository root.", "Initialize Go modules in your project:\n > ```\n > go mod init github.com/your-org/your-project\n > go mod tidy\n > git add go.mod go.sum && git commit -m \"add go module\" && git push\n > ```")
|
||||
case "missing semver release":
|
||||
return fix("No SemVer release tag (e.g. `v1.0.0`) found.", "Create a tagged release:\n > ```\n > git tag v1.0.0\n > git push origin v1.0.0\n > ```\n > Or create a release via GitHub's UI at `"+repoURL+"/releases/new`.")
|
||||
default:
|
||||
return fix(reason, "Review the [quality standards](https://github.com/avelino/awesome-go/blob/main/CONTRIBUTING.md#quality-standards).")
|
||||
}
|
||||
}
|
||||
98
.github/workflows/pr-quality-check.yaml
vendored
98
.github/workflows/pr-quality-check.yaml
vendored
@ -6,37 +6,111 @@ on:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
detect:
|
||||
name: Detect PR type
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_package_pr: ${{ steps.check.outputs.is_package_pr }}
|
||||
steps:
|
||||
- name: Check if README.md is modified
|
||||
id: check
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
files=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files --jq '.[].filename' 2>/dev/null || echo "")
|
||||
if echo "$files" | grep -q '^README.md$'; then
|
||||
echo "is_package_pr=true" >> "$GITHUB_OUTPUT"
|
||||
echo "README.md is modified — this is a package PR"
|
||||
else
|
||||
echo "is_package_pr=false" >> "$GITHUB_OUTPUT"
|
||||
echo "README.md not modified — skipping quality checks"
|
||||
fi
|
||||
|
||||
quality:
|
||||
name: Repository quality checks
|
||||
needs: detect
|
||||
if: needs.detect.outputs.is_package_pr == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
environment: action
|
||||
container: golang:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
node-version: 20
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch base branch
|
||||
run: git fetch origin ${{ github.base_ref }}
|
||||
|
||||
- name: Run quality checks
|
||||
id: check
|
||||
id: quality
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: node .github/scripts/check-quality.js
|
||||
run: go run ./.github/scripts/check-quality/
|
||||
|
||||
- name: Run diff checks
|
||||
id: diff
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITHUB_BASE_REF: ${{ github.base_ref }}
|
||||
run: go run ./.github/scripts/check-pr-diff/
|
||||
|
||||
- name: Post quality report comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-quality-check
|
||||
message: ${{ steps.check.outputs.comment }}
|
||||
message: |
|
||||
${{ steps.quality.outputs.comment }}
|
||||
|
||||
---
|
||||
|
||||
${{ steps.diff.outputs.diff_comment }}
|
||||
|
||||
- name: Sync labels
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
if: ${{ steps.check.outputs.labels != '' }}
|
||||
if: ${{ steps.quality.outputs.labels != '' }}
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
labels: ${{ join(fromJson(steps.check.outputs.labels), '\n') }}
|
||||
labels: ${{ join(fromJson(steps.quality.outputs.labels), '\n') }}
|
||||
|
||||
- name: Fail if critical checks failed
|
||||
if: ${{ steps.check.outputs.fail == 'true' }}
|
||||
if: ${{ steps.quality.outputs.fail == 'true' || steps.diff.outputs.diff_fail == 'true' }}
|
||||
run: |
|
||||
echo "Critical quality checks failed."
|
||||
echo "Quality or diff checks failed."
|
||||
exit 1
|
||||
|
||||
skip-notice:
|
||||
name: Skip quality checks (non-package PR)
|
||||
needs: detect
|
||||
if: needs.detect.outputs.is_package_pr == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Post skip notice
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-quality-check
|
||||
message: |
|
||||
## Automated Quality Checks
|
||||
|
||||
**Skipped** — this PR does not modify `README.md`, so package quality checks do not apply.
|
||||
|
||||
_This is expected for maintenance, documentation, or workflow PRs._
|
||||
|
||||
auto-merge:
|
||||
name: Enable auto-merge
|
||||
needs: quality
|
||||
if: always() && needs.quality.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Enable auto-merge via squash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh pr merge ${{ github.event.pull_request.number }} \
|
||||
--repo ${{ github.repository }} \
|
||||
--auto \
|
||||
--squash
|
||||
|
||||
46
.github/workflows/recheck-open-prs.yaml
vendored
Normal file
46
.github/workflows/recheck-open-prs.yaml
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
name: Re-check all open PRs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
recheck:
|
||||
name: Re-trigger checks on open PRs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Re-run quality checks on all open PRs
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "Fetching all open PR numbers..."
|
||||
prs=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --limit 500 --json number --jq '.[].number')
|
||||
total=$(echo "$prs" | wc -w)
|
||||
echo "Found $total open PRs"
|
||||
|
||||
count=0
|
||||
failed=0
|
||||
for pr in $prs; do
|
||||
count=$((count + 1))
|
||||
echo "[$count/$total] Re-triggering PR #$pr..."
|
||||
|
||||
# Close and reopen to trigger the 'reopened' event,
|
||||
# which re-runs pr-quality-check.yaml
|
||||
if gh pr close "$pr" --repo "$GITHUB_REPOSITORY" 2>/dev/null && \
|
||||
gh pr reopen "$pr" --repo "$GITHUB_REPOSITORY" 2>/dev/null; then
|
||||
echo " OK"
|
||||
else
|
||||
echo " FAILED (PR #$pr may be a draft or have restrictions)"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
|
||||
# Respect GitHub API rate limits (30 requests/min for mutations)
|
||||
# Each PR uses 2 calls (close + reopen), so ~2s delay keeps us safe
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Done: $((count - failed))/$total PRs re-triggered, $failed failed"
|
||||
2
.github/workflows/run-check.yaml
vendored
2
.github/workflows/run-check.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
container: golang:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Get dependencies
|
||||
run: go get -v -t -d ./...
|
||||
- name: run script
|
||||
|
||||
4
.github/workflows/site-deploy.yaml
vendored
4
.github/workflows/site-deploy.yaml
vendored
@ -15,13 +15,13 @@ jobs:
|
||||
environment: netlify
|
||||
container: golang:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Get dependencies
|
||||
run: go get -v -t -d ./...
|
||||
- name: Make awesome-go.com
|
||||
run: go run .
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
- name: deploy awesome-go.com
|
||||
|
||||
2
.github/workflows/tests.yaml
vendored
2
.github/workflows/tests.yaml
vendored
@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
container: golang:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Get dependencies
|
||||
run: go get -v -t -d ./...
|
||||
- name: Run tests
|
||||
|
||||
@ -11,10 +11,10 @@ We appreciate and recognize [all contributors](https://github.com/avelino/awesom
|
||||
|
||||
- [Quick checklist](#quick-checklist)
|
||||
- [Quality standards](#quality-standards)
|
||||
- [What is checked automatically](#what-is-checked-automatically)
|
||||
- [Preparing for review](#preparing-for-review)
|
||||
- [Local validations (optional)](#local-validations-optional)
|
||||
- [How to add an item to the list](#how-to-add-an-item-to-the-list)
|
||||
- [Examples of good and bad entries](#examples-of-good-and-bad-entries)
|
||||
- [Entry formatting rules](#entry-formatting-rules)
|
||||
- [PR body example](#pr-body-example)
|
||||
- [Congrats, your project got accepted - what now](#congrats-your-project-got-accepted---what-now)
|
||||
- [Maintenance expectations for projects listed here](#maintenance-expectations-for-projects-listed-here)
|
||||
@ -27,17 +27,17 @@ We appreciate and recognize [all contributors](https://github.com/avelino/awesom
|
||||
|
||||
## Quick checklist
|
||||
|
||||
Before opening a pull request, please ensure the following:
|
||||
Before opening a pull request, ensure the following:
|
||||
|
||||
- One PR adds, removes, or changes only one item.
|
||||
- The item is in the correct category and in alphabetical order.
|
||||
- The link text is the exact project/package name.
|
||||
- The description is concise, non-promotional, and ends with a period.
|
||||
- The repository has: at least 5 months of history, an open source license, a `go.mod`, and at least one SemVer release (vX.Y.Z).
|
||||
- Documentation in English: README and pkg.go.dev doc comments for public APIs.
|
||||
- Tests meet the coverage guideline (≥80% for non-data packages, ≥90% for data packages) when applicable.
|
||||
- Include links in the PR body to pkg.go.dev, Go Report Card, and a coverage report.
|
||||
- For ongoing development: issues and PRs are responded to within ~2 weeks; or, if the project is mature/stable, there are no bug reports older than 6 months.
|
||||
- [ ] One PR adds, removes, or changes **only one item**.
|
||||
- [ ] The item is in the **correct category** and in **alphabetical order**.
|
||||
- [ ] The link text is the **exact project/package name**.
|
||||
- [ ] The description is **concise, non-promotional, and ends with a period**.
|
||||
- [ ] The repository has: at least **5 months of history**, an **open source license**, a `go.mod`, and at least one **SemVer release** (`vX.Y.Z`).
|
||||
- [ ] Documentation in English: **README** and **pkg.go.dev doc comments** for public APIs.
|
||||
- [ ] Tests meet the coverage guideline (**≥80%** for non-data packages, **≥90%** for data packages) when applicable.
|
||||
- [ ] Include links in the PR body to **pkg.go.dev**, **Go Report Card**, and a **coverage report**.
|
||||
- [ ] For ongoing development: issues and PRs are responded to within ~2 weeks; or, if the project is mature/stable, there are no bug reports older than 6 months.
|
||||
|
||||
To set this list apart from and complement the excellent [Go wiki Projects page](https://go.dev/wiki/Projects),
|
||||
and other lists, awesome-go is a specially curated list of high-quality, actively maintained Go packages and resources.
|
||||
@ -65,6 +65,51 @@ To be on the list, project repositories should adhere to the following quality s
|
||||
|
||||
Categories must have at least 3 items.
|
||||
|
||||
## What is checked automatically
|
||||
|
||||
When you open a PR, the following checks run automatically via CI. Fixing these before submitting saves review time.
|
||||
|
||||
### Blocking checks (PR cannot be merged if these fail)
|
||||
|
||||
| Check | What it validates |
|
||||
|-------|-------------------|
|
||||
| **Repo accessible** | Repository URL responds and is not archived |
|
||||
| **go.mod present** | `go.mod` exists at the repository root |
|
||||
| **SemVer release** | At least one tag matching `vX.Y.Z` exists |
|
||||
| **pkg.go.dev reachable** | The provided pkg.go.dev link loads |
|
||||
| **Go Report Card grade** | Grade is A-, A, or A+ |
|
||||
| **PR body links present** | Forge link, pkg.go.dev, and Go Report Card are provided |
|
||||
| **Single item per PR** | Only one package added or removed per PR |
|
||||
| **Link consistency** | URL added to README matches the forge link in the PR body |
|
||||
| **Description format** | Entry ends with a period |
|
||||
| **Alphabetical order** | Entry is in the correct alphabetical position |
|
||||
| **No duplicate links** | URL is not already in the list |
|
||||
| **Entry format** | Matches `- [name](url) - Description.` pattern |
|
||||
| **Category minimum** | Category has at least 3 items |
|
||||
|
||||
### Non-blocking checks (reported as warnings)
|
||||
|
||||
| Check | What it validates |
|
||||
|-------|-------------------|
|
||||
| **Open source license** | GitHub detects a recognized OSS license |
|
||||
| **Repository maturity** | First commit is at least 5 months old |
|
||||
| **CI/CD configured** | GitHub Actions workflows are present |
|
||||
| **README present** | Repository has a README file |
|
||||
| **Coverage link** | A Codecov or Coveralls link is provided and reachable |
|
||||
| **Link text** | Link text matches the repository name |
|
||||
| **Non-promotional** | Description avoids superlative/marketing language |
|
||||
| **Extra files** | Only README.md is modified (for package additions) |
|
||||
|
||||
### Still reviewed manually by maintainers
|
||||
|
||||
- Package is in the **correct category** for its functionality
|
||||
- Package is **generally useful** to the Go community
|
||||
- Description is **accurate and clear**
|
||||
- Test coverage is **real** (not just benchmarks)
|
||||
- Documentation quality (README detail, pkg.go.dev comments)
|
||||
- Package **functions as documented**
|
||||
- For surrounding packages: still meet quality standards
|
||||
|
||||
## Preparing for review
|
||||
|
||||
Projects listed must have the following in their documentation. When submitting, you will be asked
|
||||
@ -80,26 +125,6 @@ One way to accomplish the above is to add badges to your project's README file.
|
||||
- Go to <https://goreportcard.com/> to generate a Go Report Card report, then click on the report badge in the upper-right corner to see details on how to add the badge to your README.
|
||||
- Codecov, coveralls, and gocover all offer ways to create badges for code coverage reports. Another option is to generate a badge as part of a continuous integration process. See [Code Coverage](COVERAGE.md) for an example.
|
||||
|
||||
## Local validations (optional)
|
||||
|
||||
While automated checks run on pull requests, doing a quick local validation helps speed up reviews:
|
||||
|
||||
- Repository readiness
|
||||
- Ensure `go.mod` exists and there is at least one SemVer release (e.g., `v1.2.3`).
|
||||
- Confirm the project has an open source license.
|
||||
- Documentation readiness
|
||||
- README is in English and explains what the project does and how to use it.
|
||||
- Public APIs have doc comments and the pkg.go.dev page loads.
|
||||
- Quality reports
|
||||
- Go Report Card is reachable with grade A- or better.
|
||||
- Coverage link (Codecov, Coveralls, etc.) is reachable.
|
||||
- Local coverage sanity check (optional):
|
||||
- `go test ./... -coverprofile=coverage.out`
|
||||
- `go tool cover -func=coverage.out` (check percentage)
|
||||
- List formatting
|
||||
- Correct category and alphabetical order.
|
||||
- Link text equals the project name; description ends with a period and is non-promotional.
|
||||
|
||||
## How to add an item to the list
|
||||
|
||||
Open a pull request against the README.md document that adds the repository to the list.
|
||||
@ -116,7 +141,7 @@ that the resulting list has at least 3 projects in every category, and that the
|
||||
|
||||
Fill out the template in your PR with the links asked for. If you accidentally remove the PR template from the submission, you can find it [here](https://github.com/avelino/awesome-go/blob/main/.github/PULL_REQUEST_TEMPLATE.md).
|
||||
|
||||
### Examples of good and bad entries
|
||||
### Entry formatting rules
|
||||
|
||||
Good:
|
||||
|
||||
|
||||
26
README.md
26
README.md
@ -208,6 +208,10 @@ Please take a quick gander at the [contribution guidelines](https://github.com/a
|
||||
|
||||
**[⬆ back to top](#contents)**
|
||||
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
## Actor Model
|
||||
|
||||
_Libraries for building actor-based programs._
|
||||
@ -216,7 +220,9 @@ _Libraries for building actor-based programs._
|
||||
- [Ergo](https://github.com/ergo-services/ergo) - An actor-based Framework with network transparency for creating event-driven architecture in Golang. Inspired by Erlang.
|
||||
- [Goakt](https://github.com/Tochemey/goakt) - Fast and Distributed Actor framework using protocol buffers as message for Golang.
|
||||
- [Hollywood](https://github.com/anthdm/hollywood) - Blazingly fast and light-weight Actor engine written in Golang.
|
||||
- [ProtoActor](https://github.com/asynkron/protoactor-go) - Proto Actor - Ultra fast distributed actors for Go, C# and Java/Kotlin.
|
||||
- [ProtoActor](https://github.com/asynkron/protoactor-go) - Distributed actors for Go, C#, and Java/Kotlin.
|
||||
|
||||
**[⬆ back to top](#contents)**
|
||||
|
||||
## Artificial Intelligence
|
||||
|
||||
@ -231,13 +237,11 @@ _Libraries for building programs that leverage AI._
|
||||
|
||||
**[⬆ back to top](#contents)**
|
||||
|
||||
</details>
|
||||
|
||||
## Audio and Music
|
||||
|
||||
_Libraries for manipulating audio._
|
||||
|
||||
- [beep](https://github.com/faiface/beep) - A simple library for playback and audio manipulation.
|
||||
- [beep](https://github.com/gopxl/beep) - A simple library for playback and audio manipulation.
|
||||
- [flac](https://github.com/mewkiz/flac) - Native Go FLAC encoder/decoder with support for FLAC streams.
|
||||
- [gaad](https://github.com/Comcast/gaad) - Native Go AAC bitstream parser.
|
||||
- [go-mpris](https://github.com/leberKleber/go-mpris) - Client for mpris dbus interfaces.
|
||||
@ -247,7 +251,6 @@ _Libraries for manipulating audio._
|
||||
- [malgo](https://github.com/gen2brain/malgo) - Mini audio library.
|
||||
- [minimp3](https://github.com/tosone/minimp3) - Lightweight MP3 decoder library.
|
||||
- [Oto](https://github.com/hajimehoshi/oto) - A low-level library to play sound on multiple platforms.
|
||||
- [play](https://github.com/paololazzari/play) - Command-line audio player that supports multiple formats including WAV, MP3, OGG, and FLAC.
|
||||
- [PortAudio](https://github.com/gordonklaus/portaudio) - Go bindings for the PortAudio audio I/O library.
|
||||
|
||||
**[⬆ back to top](#contents)**
|
||||
@ -365,7 +368,7 @@ _Libraries for building Console Applications and Console User Interfaces._
|
||||
|
||||
- [asciigraph](https://github.com/guptarohit/asciigraph) - Go package to make lightweight ASCII line graph ╭┈╯ in command line apps with no other dependencies.
|
||||
- [aurora](https://github.com/logrusorgru/aurora) - ANSI terminal colors that support fmt.Printf/Sprintf.
|
||||
- [box-cli-maker](https://github.com/Delta456/box-cli-maker) - Make Highly Customized Boxes for your CLI.
|
||||
- [box-cli-maker](https://github.com/box-cli-maker/box-cli-maker) - Render highly customizable boxes in the terminal.
|
||||
- [bubble-table](https://github.com/Evertras/bubble-table) - An interactive table component for bubbletea.
|
||||
- [bubbles](https://github.com/charmbracelet/bubbles) - TUI components for bubbletea.
|
||||
- [bubbletea](https://github.com/charmbracelet/bubbletea) - Go framework to build terminal apps, based on The Elm Architecture.
|
||||
@ -696,7 +699,6 @@ additional ordered map implementations.
|
||||
|
||||
_Data stores with expiring records, in-memory distributed data stores, or in-memory subsets of file-based databases._
|
||||
|
||||
- [2q](https://github.com/floatdrop/2q) - 2Q in-memory cache implementation.
|
||||
- [bcache](https://github.com/iwanbk/bcache) - Eventually consistent distributed in-memory cache Go library.
|
||||
- [BigCache](https://github.com/allegro/bigcache) - Efficient key/value cache for gigabytes of data.
|
||||
- [cache2go](https://github.com/muesli/cache2go) - In-memory key:value cache which supports automatic invalidation based on timeouts.
|
||||
@ -757,6 +759,7 @@ _Data stores with expiring records, in-memory distributed data stores, or in-mem
|
||||
- [lotusdb](https://github.com/flower-corp/lotusdb) - Fast k/v database compatible with lsm and b+tree.
|
||||
- [Milvus](https://github.com/milvus-io/milvus) - Milvus is a vector database for embedding management, analytics and search.
|
||||
- [moss](https://github.com/couchbase/moss) - Moss is a simple LSM key-value storage engine written in 100% Go.
|
||||
- [NoKV](https://github.com/feichai0017/NoKV) - High-performance distributed KV storage based on LSM Tree.
|
||||
- [nutsdb](https://github.com/xujiajun/nutsdb) - Nutsdb is a simple, fast, embeddable, persistent key/value store written in pure Go. It supports fully serializable transactions and many data structures such as list, set, sorted set.
|
||||
- [objectbox-go](https://github.com/objectbox/objectbox-go) - High-performance embedded Object Database (NoSQL) with Go API.
|
||||
- [pebble](https://github.com/cockroachdb/pebble) - RocksDB/LevelDB inspired key-value database in Go.
|
||||
@ -1571,6 +1574,7 @@ _Libraries for working with JSON._
|
||||
- [mp](https://github.com/sanbornm/mp) - Simple cli email parser. It currently takes stdin and outputs JSON.
|
||||
- [OjG](https://github.com/ohler55/ojg) - Optimized JSON for Go is a high performance parser with a variety of additional JSON tools including JSONPath.
|
||||
- [omg.jsonparser](https://github.com/dedalqq/omg.jsonparser) - Simple JSON parser with validation by condition via golang struct fields tags.
|
||||
- [SJSON](https://github.com/tidwall/sjson) - Set a JSON value with one line of code.
|
||||
- [ujson](https://github.com/olvrng/ujson) - Fast and minimal JSON parser and transformer that works on unstructured JSON.
|
||||
- [vjson](https://github.com/miladibra10/vjson) - Go package for validating JSON objects with declaring a JSON schema with fluent API.
|
||||
|
||||
@ -2053,7 +2057,7 @@ _Libraries for working with various layers of the network._
|
||||
- [net](https://golang.org/x/net) - This repository holds supplementary Go networking libraries.
|
||||
- [netpoll](https://github.com/cloudwego/netpoll) - A high-performance non-blocking I/O networking framework, which focused on RPC scenarios, developed by ByteDance.
|
||||
- [NFF-Go](https://github.com/intel-go/nff-go) - Framework for rapid development of performant network functions for cloud and bare-metal (former YANFF).
|
||||
- [nodepass](https://github.com/yosebyte/nodepass) - A secure, efficient TCP/UDP tunneling solution that delivers fast, reliable access across network restrictions using pre-established TLS/TCP connections.
|
||||
- [nodepass](https://github.com/NodePassProject/nodepass) - A secure, efficient TCP/UDP tunneling solution that delivers fast, reliable access across network restrictions using pre-established TCP/QUIC/WebSocket or HTTP/2 connections.
|
||||
- [peerdiscovery](https://github.com/schollz/peerdiscovery) - Pure Go library for cross-platform local peer discovery using UDP multicast.
|
||||
- [portproxy](https://github.com/aybabtme/portproxy) - Simple TCP proxy which adds CORS support to API's which don't support it.
|
||||
- [psql-wire](https://github.com/jeroenrinzema/psql-wire) - PostgreSQL server wire protocol. Build your own server and start serving connections..
|
||||
@ -2102,6 +2106,7 @@ _Libraries for making HTTP requests._
|
||||
- [resty](https://github.com/go-resty/resty) - Simple HTTP and REST client for Go inspired by Ruby rest-client.
|
||||
- [rq](https://github.com/ddo/rq) - A nicer interface for golang stdlib HTTP client.
|
||||
- [sling](https://github.com/dghubble/sling) - Sling is a Go HTTP client library for creating and sending API requests.
|
||||
- [surf](https://github.com/enetx/surf) - Advanced HTTP client with HTTP/1.1, HTTP/2, HTTP/3 (QUIC), SOCKS5 proxy support and browser-grade TLS fingerprinting.
|
||||
- [tls-client](https://github.com/bogdanfinn/tls-client) - net/http.Client like HTTP Client with options to select specific client TLS Fingerprints to use for requests.
|
||||
|
||||
**[⬆ back to top](#contents)**
|
||||
@ -2193,6 +2198,7 @@ _Unofficial libraries for package and dependency management._
|
||||
- [jsonql](https://github.com/elgs/jsonql) - JSON query expression library in Golang.
|
||||
- [jsonslice](https://github.com/bhmj/jsonslice) - Jsonpath queries with advanced filters.
|
||||
- [mql](https://github.com/hashicorp/mql) - Model Query Language (mql) is a query language for your database models.
|
||||
- [play](https://github.com/paololazzari/play) - A TUI playground to experiment with your favorite programs, such as grep, sed, awk, jq and yq.
|
||||
- [rql](https://github.com/a8m/rql) - Resource Query Language for REST API.
|
||||
- [rqp](https://github.com/timsolov/rest-query-parser) - Query Parser for REST API. Filtering, validations, both `AND`, `OR` operations are supported directly in the query.
|
||||
- [straf](https://github.com/SonicRoshan/straf) - Easily Convert Golang structs to GraphQL objects.
|
||||
@ -2915,6 +2921,7 @@ _General utilities and tools to make your life easier._
|
||||
- [sqlx](https://github.com/jmoiron/sqlx) - provides a set of extensions on top of the excellent built-in database/sql package.
|
||||
- [sqlz](https://github.com/rfberaldo/sqlz) - Extension for the database/sql package, adding named queries, struct scanning, and batch operations.
|
||||
- [sshman](https://github.com/shoobyban/sshman) - SSH Manager for authorized_keys files on multiple remote servers.
|
||||
- [stacktower](https://github.com/matzehuels/stacktower) - Visualize dependency graphs as physical tower structures, inspired by XKCD #2347.
|
||||
- [statiks](https://github.com/janiltonmaciel/statiks) - Fast, zero-configuration, static HTTP filer server.
|
||||
- [Storm](https://github.com/asdine/storm) - Simple and powerful toolkit for BoltDB.
|
||||
- [structs](https://github.com/PumpkinSeed/structs) - Implement simple functions to manipulate structs.
|
||||
@ -3500,7 +3507,6 @@ _Where to discover new Go libraries._
|
||||
- [GoCon](https://gocon.connpass.com/) - Tokyo, Japan.
|
||||
- [GoDays](https://www.godays.io/) - Berlin, Germany.
|
||||
- [GoLab](https://golab.io/) - Florence, Italy.
|
||||
- [GopherChina](https://gopherchina.org) - Shanghai, China.
|
||||
- [GopherCon](https://www.gophercon.com/) - Varied Locations Each Year, USA.
|
||||
- [GopherCon Australia](https://gophercon.com.au/) - Sydney, Australia.
|
||||
- [GopherCon Brazil](https://gopherconbr.org) - Florianópolis, Brazil.
|
||||
@ -3720,7 +3726,7 @@ _Add the group of your city/country here (send **PR**)_
|
||||
|
||||
### Tutorials
|
||||
|
||||
- [50 Shades of Go](https://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/) - Traps, Gotchas, and Common Mistakes for New Golang Devs.
|
||||
- [50 Shades of Go](https://golang50shades.github.io/) - Traps, Gotchas, and Common Mistakes for New Golang Devs.
|
||||
- [A Comprehensive Guide to Structured Logging in Go](https://betterstack.com/community/guides/logging/logging-in-go/) - Delve deep into the world of structured logging in Go with a specific focus on recently accepted slog proposal which aims to bring high performance structured logging with levels to the standard library.
|
||||
- [A Guide to Golang E-Commerce](https://snipcart.com/blog/golang-ecommerce-ponzu-cms-demo?utm_term=golang-ecommerce-ponzu-cms-demo) - Building a Golang site for e-commerce (demo included).
|
||||
- [A Tour of Go](https://tour.golang.org/) - Interactive tour of Go.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user