// 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< **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).") } }