Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ jobs:
go-version: stable
cache-dependency-path: "**/go.sum"
- name: Check OpenAPI
run: ./script/metadata.sh update-openapi --validate
run: |
./script/metadata.sh update-openapi --validate
./script/metadata.sh check-schema-fields
env:
CHECK_GITHUB_OPENAPI: 1
GITHUB_TOKEN: ${{ github.token }}
34 changes: 34 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,40 @@ Its subcommands are:
- `unused` - lists operations from `openapi_operations.yaml` that are not mapped
from any methods.

- `check-schema-fields` - checks Go struct JSON field optionality against
GitHub's OpenAPI schemas. A struct opts in by carrying one or more
`//meta:schema` annotations in its doc comment, each naming the operation
whose request or response body schema the struct must match:

```go
// IssueCommentRequest represents a request to create or update an issue comment.
//
//meta:schema request POST /repos/{owner}/{repo}/issues/{issue_number}/comments
//meta:schema request PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}
type IssueCommentRequest struct {
```

For every annotation the command verifies that required, non-nullable schema
fields are non-pointer fields without `omitempty` or `omitzero`, that optional
schema fields remain omittable, and that the field sets line up. Unannotated
structs are not checked, and an annotation that does not resolve to an
operation in the OpenAPI descriptions is itself reported as an issue. Run it
with:

```sh
script/metadata.sh check-schema-fields
```

When adding a new request type (or converting one to pass by value), add a
`//meta:schema request <METHOD> <path>` line per operation that uses it as a
body, reusing the method's `//meta:operation` value.

A few Go fields may intentionally deviate from the OpenAPI schema. These are
listed as `Struct.Field` entries in
`tools/metadata/schema_field_exceptions.yaml`, and their diagnostics are
suppressed; each is a known deviation to fix and remove over time. Update that
file (rather than the Go source) to add or remove an exception.

[OpenAPI descriptions of their API]: https://github.com/github/rest-api-description

## Scripts
Expand Down
4 changes: 4 additions & 0 deletions github/gists_comments.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ func (g GistComment) String() string {
}

// CreateGistCommentRequest represents the input for creating a gist comment.
//
//meta:schema request POST /gists/{gist_id}/comments
type CreateGistCommentRequest struct {
// Body is the comment text.
Body string `json:"body"`
}

// UpdateGistCommentRequest represents the input for updating a gist comment.
//
//meta:schema request PATCH /gists/{gist_id}/comments/{comment_id}
type UpdateGistCommentRequest struct {
// Body is the comment text.
Body string `json:"body"`
Expand Down
3 changes: 3 additions & 0 deletions github/issues_comments.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ func (i IssueComment) String() string {
}

// IssueCommentRequest represents a request to create or update an issue comment.
//
//meta:schema request POST /repos/{owner}/{repo}/issues/{issue_number}/comments
//meta:schema request PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}
type IssueCommentRequest struct {
Body string `json:"body"`
}
Expand Down
4 changes: 4 additions & 0 deletions github/issues_milestones.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ func (m Milestone) String() string {
}

// CreateMilestoneRequest represents a request to create a milestone.
//
//meta:schema request POST /repos/{owner}/{repo}/milestones
type CreateMilestoneRequest struct {
Title string `json:"title"`
State *string `json:"state,omitempty"`
Expand All @@ -43,6 +45,8 @@ type CreateMilestoneRequest struct {
}

// UpdateMilestoneRequest represents a request to update a milestone.
//
//meta:schema request PATCH /repos/{owner}/{repo}/milestones/{milestone_number}
type UpdateMilestoneRequest struct {
Title *string `json:"title,omitempty"`
State *string `json:"state,omitempty"`
Expand Down
4 changes: 4 additions & 0 deletions github/orgs_custom_repository_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type CustomRepoRoles struct {
}

// CreateCustomRepoRoleRequest represents the parameters to create a custom repository role.
//
//meta:schema request POST /orgs/{org}/custom-repository-roles
type CreateCustomRepoRoleRequest struct {
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Expand All @@ -39,6 +41,8 @@ type CreateCustomRepoRoleRequest struct {
}

// UpdateCustomRepoRoleRequest represents the parameters to update a custom repository role.
//
//meta:schema request PATCH /orgs/{org}/custom-repository-roles/{role_id}
type UpdateCustomRepoRoleRequest struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Expand Down
4 changes: 4 additions & 0 deletions github/pulls_comments.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ func (s *PullRequestsService) GetComment(ctx context.Context, owner, repo string

// CreatePullRequestCommentRequest represents a request to create a review
// comment on a pull request.
//
//meta:schema request POST /repos/{owner}/{repo}/pulls/{pull_number}/comments
type CreatePullRequestCommentRequest struct {
Body string `json:"body"`
CommitID string `json:"commit_id"`
Expand All @@ -160,6 +162,8 @@ type CreatePullRequestCommentRequest struct {

// UpdatePullRequestCommentRequest represents a request to update a review
// comment on a pull request.
//
//meta:schema request PATCH /repos/{owner}/{repo}/pulls/comments/{comment_id}
type UpdatePullRequestCommentRequest struct {
Body string `json:"body"`
}
Expand Down
4 changes: 4 additions & 0 deletions github/pulls_reviews.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ func (r *PullRequestReviewRequest) isComfortFadePreview() (bool, error) {
}

// PullRequestDismissReviewRequest represents a request to dismiss a review.
//
//meta:schema request PUT /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/dismissals
type PullRequestDismissReviewRequest struct {
Message string `json:"message"`
Event *string `json:"event,omitempty"`
Expand All @@ -104,6 +106,8 @@ func (r PullRequestDismissReviewRequest) String() string {
}

// PullRequestSubmitReviewRequest represents a request to submit a review.
//
//meta:schema request POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/events
type PullRequestSubmitReviewRequest struct {
Body *string `json:"body,omitempty"`
Event string `json:"event"`
Expand Down
2 changes: 2 additions & 0 deletions github/repos.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,8 @@ func (s *RepositoriesService) Create(ctx context.Context, org string, repo *Repo
}

// TemplateRepoRequest represents a request to create a repository from a template.
//
//meta:schema request POST /repos/{template_owner}/{template_repo}/generate
type TemplateRepoRequest struct {
Name string `json:"name"`
Owner *string `json:"owner,omitempty"`
Expand Down
2 changes: 2 additions & 0 deletions github/repos_autolinks.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
)

// CreateAutolinkRequest specifies parameters for RepositoriesService.CreateAutolink method.
//
//meta:schema request POST /repos/{owner}/{repo}/autolinks
type CreateAutolinkRequest struct {
KeyPrefix string `json:"key_prefix"`
URLTemplate string `json:"url_template"`
Expand Down
4 changes: 4 additions & 0 deletions github/repos_deployment_branch_policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@ type DeploymentBranchPolicyResponse struct {
}

// CreateDeploymentBranchPolicyRequest represents a request to create a deployment branch policy.
//
//meta:schema request POST /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies
type CreateDeploymentBranchPolicyRequest struct {
Name string `json:"name"`
Type *string `json:"type,omitempty"`
}

// UpdateDeploymentBranchPolicyRequest represents a request to update a deployment branch policy.
//
//meta:schema request PUT /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies/{branch_policy_id}
type UpdateDeploymentBranchPolicyRequest struct {
Name string `json:"name"`
}
Expand Down
2 changes: 2 additions & 0 deletions github/repos_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ func (s *RepositoriesService) GetKey(ctx context.Context, owner, repo string, id
}

// CreateDeployKeyRequest represents a request to create a deploy key.
//
//meta:schema request POST /repos/{owner}/{repo}/keys
type CreateDeployKeyRequest struct {
Title *string `json:"title,omitempty"`
Key string `json:"key"`
Expand Down
4 changes: 4 additions & 0 deletions github/repos_merging.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (

// RepositoryMergeRequest represents a request to merge a branch in a
// repository.
//
//meta:schema request POST /repos/{owner}/{repo}/merges
type RepositoryMergeRequest struct {
Base string `json:"base"`
Head string `json:"head"`
Expand All @@ -20,6 +22,8 @@ type RepositoryMergeRequest struct {

// RepoMergeUpstreamRequest represents a request to sync a branch of
// a forked repository to keep it up-to-date with the upstream repository.
//
//meta:schema request POST /repos/{owner}/{repo}/merge-upstream
type RepoMergeUpstreamRequest struct {
Branch string `json:"branch"`
}
Expand Down
8 changes: 8 additions & 0 deletions github/repos_releases.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ type RepositoryReleaseNotes struct {
}

// GenerateNotesRequest represents the request to generate release notes.
//
//meta:schema request POST /repos/{owner}/{repo}/releases/generate-notes
type GenerateNotesRequest struct {
TagName string `json:"tag_name"`
PreviousTagName *string `json:"previous_tag_name,omitempty"`
Expand All @@ -83,6 +85,8 @@ type ReleaseAsset struct {
}

// UpdateReleaseAssetRequest represents the request to update a release asset.
//
//meta:schema request PATCH /repos/{owner}/{repo}/releases/assets/{asset_id}
type UpdateReleaseAssetRequest struct {
Name *string `json:"name,omitempty"`
Label *string `json:"label,omitempty"`
Expand Down Expand Up @@ -185,6 +189,8 @@ func (s *RepositoriesService) getSingleRelease(ctx context.Context, url string)
}

// CreateReleaseRequest represents a request to create a release in a repository.
//
//meta:schema request POST /repos/{owner}/{repo}/releases
type CreateReleaseRequest struct {
TagName string `json:"tag_name"`
TargetCommitish *string `json:"target_commitish,omitempty"`
Expand All @@ -199,6 +205,8 @@ type CreateReleaseRequest struct {
}

// UpdateReleaseRequest represents a request to update a release in a repository.
//
//meta:schema request PATCH /repos/{owner}/{repo}/releases/{release_id}
type UpdateReleaseRequest struct {
TagName *string `json:"tag_name,omitempty"`
TargetCommitish *string `json:"target_commitish,omitempty"`
Expand Down
2 changes: 2 additions & 0 deletions github/teams.go
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,8 @@ type ExternalGroupList struct {

// UpdateConnectedExternalGroupRequest represents a request to update the connection
// between an external group and a team.
//
//meta:schema request PATCH /orgs/{org}/teams/{team_slug}/external-groups
type UpdateConnectedExternalGroupRequest struct {
GroupID int64 `json:"group_id"`
}
Expand Down
2 changes: 2 additions & 0 deletions github/users_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ func (k Key) String() string {

// CreateUserKeyRequest represents a request to create a public SSH key for the
// authenticated user.
//
//meta:schema request POST /user/keys
type CreateUserKeyRequest struct {
Title *string `json:"title,omitempty"`
Key string `json:"key"`
Expand Down
2 changes: 2 additions & 0 deletions github/users_ssh_signing_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ func (k SSHSigningKey) String() string {

// CreateSSHSigningKeyRequest represents a request to create an SSH signing key
// for the authenticated user.
//
//meta:schema request POST /user/ssh_signing_keys
type CreateSSHSigningKeyRequest struct {
Title *string `json:"title,omitempty"`
Key string `json:"key"`
Expand Down
12 changes: 10 additions & 2 deletions script/lint.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/sh
#/ [ CHECK_GITHUB_OPENAPI=1 ] script/lint.sh runs linters and validates generated files.
#/ When CHECK_GITHUB is set, it validates that openapi_operations.yaml is consistent with the
#/ descriptions from github.com/github/rest-api-description.
#/ When CHECK_GITHUB_OPENAPI is set, it validates OpenAPI metadata and schema fields
#/ against descriptions from github.com/github/rest-api-description.

set -e

Expand Down Expand Up @@ -96,6 +96,14 @@ if [ -n "$CHECK_GITHUB_OPENAPI" ]; then
printf "${RED}✘ openapi_operations.yaml validation failed${NC}\n"
fail
fi

print_header "Validating OpenAPI schema fields"
if script/metadata.sh check-schema-fields; then
printf "${GREEN}✔ OpenAPI schema fields are valid${NC}\n"
else
printf "${RED}✘ OpenAPI schema field validation failed${NC}\n"
fail
fi
fi

print_header "Validating generated files"
Expand Down
68 changes: 66 additions & 2 deletions tools/metadata/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,16 @@ Update go source code to be consistent with openapi_operations.yaml.

"format_help": `Format white space in openapi_operations.yaml and sort its operations.`,
"unused_help": `List operations in openapi_operations.yaml that aren't used by any service methods.`,
"check_schema_fields_help": `
Check Go struct JSON field optionality against GitHub's OpenAPI schemas. Only structs whose doc comment
carries one or more "//meta:schema <request|response> <METHOD> <path>" annotations are checked; each
annotation names the operation whose request or response body schema the struct must match. An annotation
that does not resolve to an operation in the OpenAPI descriptions is itself reported as an issue.
`,

"working_dir_help": `Working directory. Should be the root of the go-github repository.`,
"openapi_ref_help": `Git ref to pull OpenAPI descriptions from.`,
"working_dir_help": `Working directory. Should be the root of the go-github repository.`,
"openapi_ref_help": `Git ref to pull OpenAPI descriptions from.`,
"openapi_ref_default_help": `Git ref to pull OpenAPI descriptions from. Defaults to openapi_commit from openapi_operations.yaml.`,

"openapi_validate_help": `
Instead of updating, make sure that the operations in openapi_operations.yaml's "openapi_operations" field are
Expand All @@ -54,6 +61,7 @@ type rootCmd struct {
UpdateGo updateGoCmd `kong:"cmd,help=${update_go_help}"`
Format formatCmd `kong:"cmd,help=${format_help}"`
Unused unusedCmd `kong:"cmd,help=${unused_help}"`
CheckSchema checkSchemaCmd `kong:"cmd,name=check-schema-fields,help=${check_schema_fields_help}"`

WorkingDir string `kong:"short=C,default=.,help=${working_dir_help}"`

Expand Down Expand Up @@ -182,6 +190,62 @@ func (c *unusedCmd) Run(root *rootCmd, k *kong.Context) error {
return nil
}

type checkSchemaCmd struct {
Ref string `kong:"help=${openapi_ref_default_help}"`
Verbose bool `kong:"help='Print each checked annotation.'"`
}

func (c *checkSchemaCmd) Run(root *rootCmd, k *kong.Context) error {
ctx := context.Background()
_, opsFile, err := root.opsFile()
if err != nil {
return err
}
ref := c.Ref
if ref == "" {
ref = opsFile.GitCommit
if ref == "" {
return errors.New("openapi_operations.yaml does not have an openapi_commit field")
}
}

client, err := githubClient(root.GithubURL, root.UploadURL)
if err != nil {
return err
}
descriptions, err := getDescriptions(ctx, client, ref)
if err != nil {
return err
}
exceptions, err := loadSchemaFieldExceptions(root.WorkingDir)
if err != nil {
return err
}
result, err := checkSchemaFields(schemaFieldCheckOptions{
descriptions: descriptions,
githubDir: filepath.Join(root.WorkingDir, "github"),
exceptions: exceptions,
})
if err != nil {
return err
}

fmt.Fprintf(k.Stdout, "Found %v schema field issues\n", len(result.Diagnostics))
fmt.Fprintf(k.Stdout, "Checked %v annotations on %v annotated structs\n", result.Summary.Checked, result.Summary.AnnotatedStructs)
if c.Verbose {
for _, checked := range result.Checked {
fmt.Fprintf(k.Stdout, "checked: %v -> %v\n", checked.GoStruct, checked.Annotation)
}
}
for _, diag := range result.Diagnostics {
fmt.Fprintln(k.Stdout, diag.String())
}
if len(result.Diagnostics) > 0 {
return fmt.Errorf("found %v schema field issues", len(result.Diagnostics))
}
return nil
}

func main() {
err := run(os.Args[1:], nil)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions tools/metadata/schema_field_exceptions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The file lists "Struct.Field" entries whose JSON field optionality intentionally deviates from the OpenAPI schema,
# so that their check-schema-fields diagnostics are suppressed. Add an entry only for an annotated struct whose
# deviation is deliberate; unannotated structs are not checked at all.
exceptions: []
Loading
Loading