Skip to content

fix: type and validate GitHub responses against real schemas - #23

Merged
blimmer merged 4 commits into
mainfrom
feat/github-typed-clients
May 27, 2026
Merged

fix: type and validate GitHub responses against real schemas#23
blimmer merged 4 commits into
mainfrom
feat/github-typed-clients

Conversation

@blimmer

@blimmer blimmer commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to the PATCHWAVE-ANALYSIS-CLI-1 hot-fix (#21). The root cause was that GithubClient took the response type as a blind caller-supplied generic (paginate<T>(route: string, …)), so every collector hand-rolled a response interface that could drift from GitHub's real API. This binds our types to the published schemas and adds a runtime safety net for the cases types can't catch:

  • RESTpaginate/request are now generic over the route literal, deriving params + response types from octokit's Endpoints / PaginatingEndpoints. Field typos and genuinely-absent/nullable fields now fail at compile time.
  • GraphQLgraphql() takes a TypedDocumentNode; the Dependabot-PR query moved to a .graphql file with types generated by graphql-codegen against @octokit/graphql-schema (no more hand-rolled RawPullRequest/RawActor).
  • Runtime validationpaginate() accepts a Zod schema reflecting reality; invalid items are dropped with a warning instead of crashing the scan. Collectors keep ownership of normalization (schema + raw→domain map), so everything downstream still sees only clean domain types.

Build wiring is consolidated behind one bun run build:assets step (graphql codegen + report-web) referenced by package scripts, the justfile, and goreleaser. The generated module is committed and excluded from lint/format.

just verify passes (format, typecheck, lint, 118 unit + 20 browser tests). Codegen output is deterministic (regenerating leaves no diff).

Review focus

  • Why runtime validation is the real fix, not the types. I initially claimed octokit's types would compile-catch the original author.login crash — they don't. octokit models the empty-author arm as Record<string, never>, whose index signature makes author.login type-check as string even with noUncheckedIndexedAccess on. So the typed seam catches drift/typos/nullable-unions, but the Zod validation in contributors.ts (and the others) is what actually prevents the empty-object class. Worth sanity-checking that reasoning.
  • The route-typed seam casts to octokit's loose overloads (route as string) at the transport boundary in GithubClientImpl — the public interface is the source of truth; the cast is contained there.
  • Schema strictness vs. dropping data. repoSchema requires only name+owner; everything else is optional/defaulted so a repo is never dropped for a field GitHub omits. cve's security_vulnerability is nullable (null alerts skipped). Confirm these match the behavior you want.

Commits

Single commit — the seam change doesn't compile until every collector migrates, so it's one logical unit.

  • 4d480a5 — type and validate GitHub responses against real schemas

Manual verification not yet done

  • A live bun run start <org> smoke run against a real org (needs a token + network; not run in this environment).

The GithubClient seam took the response type as a blind caller-supplied
generic (paginate<T>(route: string, ...)), so every collector hand-rolled
its own response interface that could silently drift from GitHub's real
API — the root cause behind PATCHWAVE-ANALYSIS-CLI-1.

This binds our types to the published schemas and adds a runtime safety
net for the cases types can't catch:

- REST: paginate/request are now generic over the route literal, deriving
  params and response types from octokit's Endpoints / PaginatingEndpoints.
  Field typos and genuinely-absent/nullable fields now fail at compile time.
- GraphQL: graphql() takes a TypedDocumentNode; dependabotPrs' query moves to
  a .graphql file and its types are generated by graphql-codegen against
  @octokit/graphql-schema. No more hand-rolled RawPullRequest/RawActor.
- Runtime validation: paginate() accepts a Zod schema reflecting reality
  (e.g. a commit author may be null/loginless, a dependabot alert's
  security_vulnerability may be null). Invalid items are dropped with a
  warning instead of crashing the scan. This is the actual guard for the
  empty-object class: octokit models it as Record<string, never>, whose
  index signature makes author.login type-check as string, so the bug is
  not catchable at compile time alone.

Collectors keep ownership of normalization: each supplies its schema and
maps raw -> domain, so everything downstream still sees only clean types.

Build wiring is consolidated behind one `bun run build:assets` step
(graphql codegen + report-web) referenced by package scripts, the justfile,
and goreleaser. The generated module is committed and excluded from
lint/format.
@blimmer
blimmer requested a review from jcarver989 as a code owner May 27, 2026 19:59
@blimmer
blimmer marked this pull request as draft May 27, 2026 20:05
Re-run graphql-codegen in the lint job and fail on any diff, so a query
change without a regenerate can't merge. Mark the committed output as
linguist-generated via .gitattributes so GitHub collapses it in diffs.
@blimmer
blimmer force-pushed the feat/github-typed-clients branch from 04e0781 to d450b82 Compare May 27, 2026 20:07
@@ -0,0 +1,71 @@
query DependabotPrs($searchQuery: String!, $cursor: String) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using legit graphql-codegen now against their gql schemas

author: { login?: string; type?: string } | null;
commit: { author: { name: string; date: string } | null };
}
// Treat unmatched/deleted GitHub authors as anonymous and skip them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes they don't even follow their own schema, like here.

@blimmer
blimmer marked this pull request as ready for review May 27, 2026 20:19
@blimmer
blimmer merged commit b00cde7 into main May 27, 2026
8 checks passed
@blimmer
blimmer deleted the feat/github-typed-clients branch May 27, 2026 20:22
blimmer pushed a commit that referenced this pull request May 27, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.2.3](v0.2.2...v0.2.3)
(2026-05-27)


### Bug Fixes

* open the report automatically instead of prompting
([#24](#24))
([ba580d9](ba580d9))
* type and validate GitHub responses against real schemas
([#23](#23))
([b00cde7](b00cde7))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: contextbridge-pr-automation[bot] <259134118+contextbridge-pr-automation[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant