Skip to content

chore: add support for validating README files for modules #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
73f3ea2
refactor: move to cmd dir
Parkreiner Apr 14, 2025
a2abeae
fix: update script references for CI
Parkreiner Apr 14, 2025
860a633
wip: add support for reading env from CI
Parkreiner Apr 14, 2025
ec1b4a7
test: see how CI output works
Parkreiner Apr 14, 2025
0a597c2
test: try logging refs
Parkreiner Apr 14, 2025
d2c5f8d
fix: actually add the test calls
Parkreiner Apr 14, 2025
25d301c
wip: commit progress
Parkreiner Apr 14, 2025
9f03579
wip: commit progress
Parkreiner Apr 15, 2025
3fa316d
wip: get basic GH API call stuff working
Parkreiner Apr 15, 2025
6e5d960
refactor: split README logic into separate file
Parkreiner Apr 15, 2025
94ca584
refactor: move where validation phase is defined
Parkreiner Apr 15, 2025
18680d0
chore: add directory validation in separate file
Parkreiner Apr 15, 2025
17c9667
wip: commit progress for module validation
Parkreiner Apr 15, 2025
aa0b871
wip: commit progress on validation
Parkreiner Apr 15, 2025
e888506
wip: get concurrency stubs set up
Parkreiner Apr 15, 2025
19226af
wip: commit more progress
Parkreiner Apr 15, 2025
f23bbca
refactor: start making main() leaner
Parkreiner Apr 16, 2025
135d5c8
fix: remove potential race condition
Parkreiner Apr 16, 2025
6eef059
wip: commit progress on git manipulation
Parkreiner Apr 16, 2025
a00a9ce
wip: commit progress
Parkreiner Apr 16, 2025
f10f5a4
chore: add sample data
Parkreiner Apr 16, 2025
e6efd71
wip: more progress
Parkreiner Apr 16, 2025
d2ebc2b
fix: update icon URLs
Parkreiner Apr 16, 2025
02817d9
Merge branch 'main' into mes/mod-temp-valid
Parkreiner Apr 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# This should be the value of the GitHub Actions actor who triggered a run. The
# CI script will inject this value from the GitHub Actions context to verify
# whether changing certain README fields is allowed. In local development, you
# can set this to your GitHub username.
CI_ACTOR=

# This is the configurable base URL for accessing the GitHub REST API. This
# value will be injected by the CI script's Actions context, but if the value is
# not defined (either in CI or when running locally), "https://api.github.com/"
# will be used as a fallback.
GITHUB_API_URL=

# This is the API token for the user that will be used to authenticate calls to
# the GitHub API. In CI, the value will be loaded with a token belonging to a
# Coder Registry admin to verify whether modifying certain README fields is
# allowed. In local development, you can set a token with the read:org
# permission. If the loaded token does not belong to a Coder employee, certain
# README verification steps will be skipped.
GITHUB_API_TOKEN=
10 changes: 8 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ concurrency:
jobs:
validate-readme-files:
runs-on: ubuntu-latest
env:
ACTOR: ${{ github.actor }}
BASE_REF: ${{ github.base_ref }}
HEAD_REF: ${{ github.head_ref }}
GITHUB_API_URL: ${{ github.api_url }}
GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Check out code
uses: actions/checkout@v4
Expand All @@ -17,9 +23,9 @@ jobs:
with:
go-version: "1.23.2"
- name: Validate contributors
run: go build ./scripts/contributors && ./contributors
run: go build ./cmd/readmevalidation && ./readmevalidation
- name: Remove build file artifact
run: rm ./contributors
run: rm ./readmevalidation
test-terraform:
runs-on: ubuntu-latest
steps:
Expand Down
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,9 @@ dist
.yarn/install-state.gz
.pnp.*

# Script output
/contributors
# Things needed for CI
/readmevalidation
/readmevalidation-git

# Terraform files generated during testing
.terraform*
Expand Down
169 changes: 169 additions & 0 deletions cmd/github/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Package github provides utilities to make it easier to deal with various
// GitHub APIs
package github

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
)

const defaultGithubAPIBaseRoute = "https://api.github.com/"

// Client is a reusable REST client for making requests to the GitHub API.
// It should be instantiated via NewGithubClient
type Client struct {
baseURL string
token string
httpClient http.Client
}

// ClientInit is used to instantiate a new client. If the value of BaseURL is
// not defined, a default value of "https://api.github.com/" is used instead
type ClientInit struct {
BaseURL string
APIToken string
}

// NewClient instantiates a GitHub client. If the baseURL is
func NewClient(init ClientInit) (*Client, error) {
// Considered letting the user continue on with no token and more aggressive
// rate-limiting, but from experimentation, the non-authenticated experience
// hit the rate limits really quickly, and had a lot of restrictions
apiToken := init.APIToken
if apiToken == "" {
return nil, errors.New("API token is missing")
}

baseURL := init.BaseURL
if baseURL == "" {
baseURL = defaultGithubAPIBaseRoute
}

return &Client{
baseURL: baseURL,
token: apiToken,
httpClient: http.Client{Timeout: 10 * time.Second},
}, nil
}

// User represents a truncated version of the API response from Github's /user
// endpoint.
type User struct {
Login string `json:"login"`
}

// GetUserFromToken returns the user associated with the loaded API token
func (gc *Client) GetUserFromToken() (User, error) {
req, err := http.NewRequest("GET", gc.baseURL+"user", nil)
if err != nil {
return User{}, err
}
if gc.token != "" {
req.Header.Add("Authorization", "Bearer "+gc.token)
}

res, err := gc.httpClient.Do(req)
if err != nil {
return User{}, err
}
defer res.Body.Close()

if res.StatusCode == http.StatusUnauthorized {
return User{}, errors.New("request is not authorized")
}
if res.StatusCode == http.StatusForbidden {
return User{}, errors.New("request is forbidden")
}

b, err := io.ReadAll(res.Body)
if err != nil {
return User{}, err
}

user := User{}
if err := json.Unmarshal(b, &user); err != nil {
return User{}, err
}
return user, nil
}

// OrgStatus indicates whether a GitHub user is a member of a given organization
type OrgStatus int

var _ fmt.Stringer = OrgStatus(0)

const (
// OrgStatusIndeterminate indicates when a user's organization status
// could not be determined. It is the zero value of the OrgStatus type, and
// any users with this value should be treated as completely untrusted
OrgStatusIndeterminate = iota

// OrgStatusNonMember indicates when a user is definitely NOT part of an
// organization
OrgStatusNonMember

// OrgStatusMember indicates when a user is a member of a Github
// organization
OrgStatusMember
)

func (s OrgStatus) String() string {
switch s {
case OrgStatusMember:
return "Member"
case OrgStatusNonMember:
return "Non-member"
default:
return "Indeterminate"
}
}

// GetUserOrgStatus takes a GitHub username, and checks the GitHub API to see
// whether that member is part of the provided organization
func (gc *Client) GetUserOrgStatus(orgName string, username string) (OrgStatus, error) {
// This API endpoint is really annoying, because it's able to produce false
// negatives. Any user can be:
// 1. A public member of an organization
// 2. A private member of an organization
// 3. Not a member of an organization
//
// So if the function returns status 200, you can always trust that. But if
// it returns any 400 code, that could indicate a few things:
// 1. The user associated with the token is a member of the organization,
// and the user being checked is not.
// 2. The user associated with the token is NOT a member of the
// organization, and the member being checked is a private member. The
// token user will have no way to view the private member's status.
// 3. Neither the user being checked nor the user associated with the token
// are members of the organization.
//
// The best option to avoid false positives is to make sure that the token
// being used belongs to a member of the organization being checked.
url := fmt.Sprintf("%sorgs/%s/members/%s", gc.baseURL, orgName, username)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return OrgStatusIndeterminate, err
}
if gc.token != "" {
req.Header.Add("Authorization", "Bearer "+gc.token)
}

res, err := gc.httpClient.Do(req)
if err != nil {
return OrgStatusIndeterminate, err
}
defer res.Body.Close()

switch res.StatusCode {
case http.StatusNoContent:
return OrgStatusMember, nil
case http.StatusNotFound:
return OrgStatusNonMember, nil
default:
return OrgStatusIndeterminate, nil
}
}
Loading
Loading