Skip to content

Commit 25d301c

Browse files
committed
wip: commit progress
1 parent d2c5f8d commit 25d301c

File tree

6 files changed

+153
-12
lines changed

6 files changed

+153
-12
lines changed

.env_example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ACTOR=
2+
BASE_REF=
3+
HEAD_REF=
4+
GITHUB_API_URL=
5+
GITHUB_API_TOKEN=

.github/workflows/ci.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ jobs:
1010
validate-contributors:
1111
runs-on: ubuntu-latest
1212
env:
13-
actor: ${{ github.actor }}
14-
base_ref: ${{ github.base_ref }}
15-
head_ref: ${{ github.head_ref }}
13+
ACTOR: ${{ github.actor }}
14+
BASE_REF: ${{ github.base_ref }}
15+
HEAD_REF: ${{ github.head_ref }}
16+
GITHUB_API_URL: ${{ github.api_url }}
17+
GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1618
steps:
1719
- name: Check out code
1820
uses: actions/checkout@v4

cmd/github/github.go

Lines changed: 125 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,28 @@
33
package github
44

55
import (
6+
"encoding/json"
67
"errors"
78
"fmt"
9+
"io"
10+
"log"
11+
"net/http"
812
"os"
13+
"strings"
14+
"time"
915
)
1016

17+
const defaultGithubAPIRoute = "https://api.github.com/"
18+
1119
const (
12-
actionsActorKey = "actor"
13-
actionsBaseRefKey = "base_ref"
14-
actionsHeadRefKey = "head_ref"
20+
actionsActorKey = "ACTOR"
21+
actionsBaseRefKey = "BASE_REF"
22+
actionsHeadRefKey = "HEAD_REF"
23+
)
24+
25+
const (
26+
githubAPIURLKey = "GITHUB_API_URL"
27+
githubAPITokenKey = "GITHUB_API_TOKEN"
1528
)
1629

1730
// ActionsActor returns the username of the GitHub user who triggered the
@@ -20,7 +33,7 @@ const (
2033
func ActionsActor() (string, error) {
2134
username := os.Getenv(actionsActorKey)
2235
if username == "" {
23-
return "", fmt.Errorf("value for %q is not in env. Please update the CI script to load the value in during CI", actionsActorKey)
36+
return "", fmt.Errorf("value for %q is not in env. If running from CI, please add value via ci.yaml file", actionsActorKey)
2437
}
2538
return username, nil
2639
}
@@ -31,8 +44,113 @@ func ActionsActor() (string, error) {
3144
func ActionsRefs() (string, string, error) {
3245
baseRef := os.Getenv(actionsBaseRefKey)
3346
headRef := os.Getenv(actionsHeadRefKey)
34-
fmt.Println("Base ref: ", baseRef)
35-
fmt.Println("Head ref: ", headRef)
3647

37-
return "", "", errors.New("we ain't ready yet")
48+
if baseRef == "" && headRef == "" {
49+
return "", "", fmt.Errorf("values for %q and %q are not in env. If running from CI, please add values via ci.yaml file", actionsHeadRefKey, actionsBaseRefKey)
50+
} else if headRef == "" {
51+
return "", "", fmt.Errorf("value for %q is not in env. If running from CI, please add value via ci.yaml file", actionsHeadRefKey)
52+
} else if baseRef == "" {
53+
return "", "", fmt.Errorf("value for %q is not in env. If running from CI, please add value via ci.yaml file", actionsBaseRefKey)
54+
}
55+
56+
return headRef, baseRef, nil
57+
}
58+
59+
// CoderEmployees represents all members of the Coder GitHub organization. This
60+
// value should not be instantiated from outside the package, and should instead
61+
// be created via one of the package's exported functions.
62+
type CoderEmployees struct {
63+
// Have map defined as private field to make sure that it can't ever be
64+
// mutated from an outside package
65+
_employees map[string]struct{}
66+
}
67+
68+
// IsEmployee takes a GitHub username and indicates whether the matching user is
69+
// a member of the Coder organization
70+
func (ce *CoderEmployees) IsEmployee(username string) bool {
71+
if ce._employees == nil {
72+
return false
73+
}
74+
75+
_, ok := ce._employees[username]
76+
return ok
77+
}
78+
79+
// TotalEmployees returns the number of members in the Coder organization
80+
func (ce *CoderEmployees) TotalEmployees() int {
81+
return len(ce._employees)
82+
}
83+
84+
type ghOrganizationMember struct {
85+
Login string `json:"login"`
86+
}
87+
88+
type ghRateLimitedRes struct {
89+
Message string `json:"message"`
90+
}
91+
92+
func parseResponse[V any](b []byte) (V, error) {
93+
var want V
94+
var rateLimitedRes ghRateLimitedRes
95+
96+
if err := json.Unmarshal(b, &rateLimitedRes); err != nil {
97+
return want, err
98+
}
99+
if isRateLimited := strings.Contains(rateLimitedRes.Message, "API rate limit exceeded for "); isRateLimited {
100+
return want, errors.New("request was rate-limited")
101+
}
102+
if err := json.Unmarshal(b, &want); err != nil {
103+
return want, err
104+
}
105+
106+
return want, nil
107+
}
108+
109+
// CoderEmployeeUsernames requests from the GitHub API the list of all usernames
110+
// of people who are employees of Coder.
111+
func CoderEmployeeUsernames() (CoderEmployees, error) {
112+
apiURL := os.Getenv(githubAPIURLKey)
113+
if apiURL == "" {
114+
log.Printf("API URL not set via env key %q. Defaulting to %q\n", githubAPIURLKey, defaultGithubAPIRoute)
115+
apiURL = defaultGithubAPIRoute
116+
}
117+
token := os.Getenv(githubAPITokenKey)
118+
if token == "" {
119+
log.Printf("API token not set via env key %q. All requests will be non-authenticated and subject to more aggressive rate limiting", githubAPITokenKey)
120+
}
121+
122+
req, err := http.NewRequest("GET", apiURL+"/orgs/coder/members", nil)
123+
if err != nil {
124+
return CoderEmployees{}, fmt.Errorf("coder employee names: %v", err)
125+
}
126+
if token != "" {
127+
req.Header.Add("Authorization", "Bearer "+token)
128+
}
129+
130+
client := http.Client{Timeout: 5 * time.Second}
131+
res, err := client.Do(req)
132+
if err != nil {
133+
return CoderEmployees{}, fmt.Errorf("coder employee names: %v", err)
134+
}
135+
defer res.Body.Close()
136+
if res.StatusCode != http.StatusOK {
137+
return CoderEmployees{}, fmt.Errorf("coder employee names: got back status code %d", res.StatusCode)
138+
}
139+
140+
b, err := io.ReadAll(res.Body)
141+
if err != nil {
142+
return CoderEmployees{}, fmt.Errorf("coder employee names: %v", err)
143+
}
144+
rawMembers, err := parseResponse[[]ghOrganizationMember](b)
145+
if err != nil {
146+
return CoderEmployees{}, fmt.Errorf("coder employee names: %v", err)
147+
}
148+
149+
employeesSet := map[string]struct{}{}
150+
for _, m := range rawMembers {
151+
employeesSet[m.Login] = struct{}{}
152+
}
153+
return CoderEmployees{
154+
_employees: employeesSet,
155+
}, nil
38156
}

cmd/readmevalidation/main.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,30 @@ import (
1010
"log"
1111

1212
"coder.com/coder-registry/cmd/github"
13+
"github.com/joho/godotenv"
1314
)
1415

1516
func main() {
17+
err := godotenv.Load()
18+
if err != nil {
19+
log.Panic(err)
20+
}
1621
username, err := github.ActionsActor()
1722
if err != nil {
1823
log.Panic(err)
1924
}
20-
log.Printf("running as %q\n", username)
21-
_, _, err = github.ActionsRefs()
25+
log.Printf("Running validation for user %q", username)
26+
headRef, baseRef, err := github.ActionsRefs()
27+
if err != nil {
28+
log.Panic(err)
29+
}
30+
log.Printf("Using branches %q and %q for validation comparison", headRef, baseRef)
31+
32+
employees, err := github.CoderEmployeeUsernames()
2233
if err != nil {
2334
log.Panic(err)
2435
}
36+
log.Printf("got back %d employees\n", employees.TotalEmployees())
2537

2638
log.Println("Starting README validation")
2739
allReadmeFiles, err := aggregateContributorReadmeFiles()

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ module coder.com/coder-registry
33
go 1.23.2
44

55
require gopkg.in/yaml.v3 v3.0.1
6+
7+
require github.com/joho/godotenv v1.5.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
2+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
13
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
24
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
35
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

0 commit comments

Comments
 (0)