fleet/tools/qacheck/main.go
Sharon Katz bdf6767700
A tool to check that the testplan checkbox was checked (#39948)
# qacheck

Scans a GitHub Project v2 for items in ✔️Awaiting QA
that are missing or have an unchecked QA confirmation checklist.

## Build

export GITHUB_TOKEN=...
go mod tidy
go build -o qacheck .

## Run

./qacheck -org fleetdm -project 71
./qacheck -org fleetdm -project 97

---------

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-23 09:24:14 -05:00

212 lines
4.4 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"strings"
"github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
)
const (
awaitingQAColumn = "✔Awaiting QA"
checkText = "Engineer: Added comment to user story confirming successful completion of test plan."
)
type Item struct {
ID githubv4.ID
Content struct {
Issue struct {
Number githubv4.Int
Title githubv4.String
Body githubv4.String
URL githubv4.URI
} `graphql:"... on Issue"`
PullRequest struct {
Number githubv4.Int
Title githubv4.String
Body githubv4.String
URL githubv4.URI
} `graphql:"... on PullRequest"`
} `graphql:"content"`
FieldValues struct {
Nodes []struct {
SingleSelectValue struct {
Name githubv4.String
} `graphql:"... on ProjectV2ItemFieldSingleSelectValue"`
}
} `graphql:"fieldValues(first: 20)"`
}
func main() {
org := flag.String("org", "", "GitHub org")
projectNum := flag.Int("project", 0, "Project number")
limit := flag.Int("limit", 100, "Max project items to scan (no pagination; expected usage is small)")
flag.Parse()
if *org == "" || *projectNum == 0 {
log.Fatal("org and project are required")
}
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
log.Fatal("GITHUB_TOKEN env var is required")
}
ctx := context.Background()
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
client := githubv4.NewClient(oauth2.NewClient(ctx, src))
projectID := fetchProjectID(ctx, client, *org, *projectNum)
items := fetchItems(ctx, client, projectID, *limit)
var bad []Item
for _, it := range items {
if !inAwaitingQA(it) {
continue
}
if hasUncheckedTestPlanLine(getBody(it)) {
bad = append(bad, it)
}
}
fmt.Printf(
"\nFound %d items in %q with UNCHECKED test-plan confirmation:\n\n",
len(bad),
awaitingQAColumn,
)
for _, it := range bad {
fmt.Printf(
"❌ #%d %s\n %s\n\n",
getNumber(it),
getTitle(it),
getURL(it),
)
}
}
func fetchProjectID(ctx context.Context, client *githubv4.Client, org string, num int) githubv4.ID {
var q struct {
Organization struct {
ProjectV2 struct {
ID githubv4.ID
} `graphql:"projectV2(number: $num)"`
} `graphql:"organization(login: $org)"`
}
err := client.Query(ctx, &q, map[string]interface{}{
"org": githubv4.String(org),
"num": githubv4.Int(num),
})
if err != nil {
log.Fatalf("project query failed: %v", err)
}
return q.Organization.ProjectV2.ID
}
func fetchItems(
ctx context.Context,
client *githubv4.Client,
projectID githubv4.ID,
limit int,
) []Item {
var q struct {
Node struct {
ProjectV2 struct {
Items struct {
Nodes []Item
} `graphql:"items(first: $first)"`
} `graphql:"... on ProjectV2"`
} `graphql:"node(id: $id)"`
}
err := client.Query(ctx, &q, map[string]interface{}{
"id": projectID,
"first": githubv4.Int(limit),
})
if err != nil {
log.Fatalf("items query failed: %v", err)
}
if len(q.Node.ProjectV2.Items.Nodes) == limit {
log.Printf(
"NOTE: scanned %d items (limit reached, no pagination by design). Increase -limit if needed.",
limit,
)
}
return q.Node.ProjectV2.Items.Nodes
}
func inAwaitingQA(it Item) bool {
for _, v := range it.FieldValues.Nodes {
if string(v.SingleSelectValue.Name) == awaitingQAColumn {
return true
}
}
return false
}
// Only flag if the unchecked checklist line exists.
// Ignore if missing or checked.
func hasUncheckedTestPlanLine(body string) bool {
if body == "" {
return false
}
unchecked1 := "- [ ] " + checkText
unchecked2 := "[ ] " + checkText
checked := []string{
"- [x] " + checkText,
"- [X] " + checkText,
"[x] " + checkText,
"[X] " + checkText,
}
for _, c := range checked {
if strings.Contains(body, c) {
return false
}
}
return strings.Contains(body, unchecked1) || strings.Contains(body, unchecked2)
}
func getBody(it Item) string {
if it.Content.Issue.Number != 0 {
return string(it.Content.Issue.Body)
}
return string(it.Content.PullRequest.Body)
}
func getTitle(it Item) string {
if it.Content.Issue.Number != 0 {
return string(it.Content.Issue.Title)
}
return string(it.Content.PullRequest.Title)
}
func getNumber(it Item) int {
if it.Content.Issue.Number != 0 {
return int(it.Content.Issue.Number)
}
return int(it.Content.PullRequest.Number)
}
func getURL(it Item) string {
if it.Content.Issue.Number != 0 {
return it.Content.Issue.URL.String()
}
return it.Content.PullRequest.URL.String()
}