Documentation
¶
Overview ¶
Package graphqlschema holds the pinned GitLab GraphQL schema and validates documents against it.
Every GraphQL test in this repository answers the request from an httptest handler that returns whatever the test wrote, so the document itself used to be judged by nobody: our code and our fixture agreed with each other and both could be wrong about GitLab. Four registered tools shipped with green tests and documents no current instance accepts, and the same blindness let eight domains advertise a backward pagination no operation declared.
GitLab is the only party that refuses a document, and no unit test may reach it, so the schema comes here instead. cmd/gen_graphql_schema introspects a live instance and writes gitlab-schema.graphql beside source.json, which records the instance, the version it reported and the day it answered, so a reader can tell how old the pin is. The SDL is committed as text, close to a megabyte of it, because a re-pin is the one moment somebody needs to read what GitLab changed; git stores two revisions of the text in about half the space it needs for two gzip streams, which it can neither delta nor diff.
Which release the pin names ¶
The pin is always gitlab.com's, and that is a choice rather than a limit: cmd/gen_graphql_schema --check refuses a pin of any other instance, because the gate would then promise what that instance accepts rather than what this server targets. An unlicensed gitlab-ee image serves the whole Enterprise schema too, since GitLab builds it when the process boots, and .github/workflows/ee-schema.yml introspects one every week; it is a check, not the reference. gitlab.com runs the next minor's pre-release, so the version source.json records is one no self-managed instance runs yet, and one minor ahead of the released gitlab-ee image the REST record (docs/development/gitlab-api-live.json) is taken from. The two name the same minor only once the REST record is taken from the release the GraphQL pin anticipates.
Being ahead costs something in each direction. A field the pre-release adds is accepted here and refused by every released instance: the release pin below refuses it on the commit that selects it, the weekly ee-schema.yml re-probe judges every document against the release that shipped last, and a domain whose document needs a recent field states the oldest GitLab it works on. A field GitLab removes leaves gitlab.com first, so the pin refuses a document that stopped working there while it still works on every self-managed instance, and nothing lets a document declare why; docs/development/graphql.md records that as deliberate.
The release pin ¶
A second schema sits beside the first: gitlab-release-schema.graphql, with release-source.json, is the schema of the GitLab release the REST record and the fine-grained table are taken from, rendered the way the gitlab.com pin is, and ReleaseSchema loads it. cmd/gen_graphql_schema -release writes it from the schema dump GitLab's own CI keeps for the release's tag, and its -check holds the record to the REST record's release and revision, so the two move together. cmd/audit_graphql_documents judges every document this server sends against it after the gitlab.com pin, which is what a pre-release field needs: the re-pin of 2026-10-29 let crashState into a selection seven security actions share, every gate that read this pin stayed green, and GitLab 19.5 refused the seven. The test transport keeps to the gitlab.com pin, since the values a caller sends may name what only a newer GitLab has on purpose.
Cost ¶
Parsing the schema takes around 200 ms for the number of types source.json records, so it happens once per process behind a sync.Once and never per call. Validating one document against the loaded schema costs tens of microseconds, which is what makes running it inside the shared test transport affordable.
What it cannot see ¶
The pin is a snapshot, and GitLab narrows fields in place: the securityReportFindings confidence argument and the [String!] typing of Project.vulnerabilities.severity were both valid once. A document this package accepts is one the pinned instance accepted on the day recorded in source.json, which is a far stronger statement than the mocks used to make and still not the same as one a live instance accepts today. cmd/audit_graphql_documents -schema is how that last gap is closed on demand, against a schema fetched rather than pinned.
Three limits are inherent to validating at this layer rather than at GitLab's, and are worth knowing before assuming more of a green run than it offers. A fractional number passes where Int is declared, because JSON has already made every number a float64 by the time the value is seen. A custom scalar accepts anything, so a malformed global id passes as a VulnerabilityID. And no depth or complexity limit is enforced, so a query GitLab would refuse as too expensive is accepted here. The pin also carries no deprecation marks, so it reports a document that is already broken and never one that is about to be.
Enum case is not among those limits, and used to be. gqlparser compares an enum value with strings.EqualFold, so it accepts "critical" for VulnerabilitySeverity, while GitLab answers that it expected one of INFO, UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL and executes nothing. Validate walks the variables itself for that one reason.
Index ¶
- Constants
- func Load(sdl []byte) (*ast.Schema, error)
- func ParseAgainst(schema *ast.Schema, document string) (*ast.QueryDocument, error)
- func ReleaseSchema() (*ast.Schema, error)
- func Schema() (*ast.Schema, error)
- func Validate(document string, variables map[string]any) error
- func ValidateDocument(document string) error
- func ValidateDocumentAgainst(schema *ast.Schema, document string) error
- type ReleaseSource
- type Source
- type ValidationError
Constants ¶
const ReleaseSDLFileName = "gitlab-release-schema.graphql"
ReleaseSDLFileName is the release pin's schema on disk: the GraphQL schema of the GitLab release the REST record (docs/development/gitlab-api-live.json) and the fine-grained table are taken from, rendered the way the gitlab.com pin is. Hyphenated for the reason SDLFileName gives.
const ReleaseSourceFileName = "release-source.json"
ReleaseSourceFileName is the release pin's provenance record, written by cmd/gen_graphql_schema -release beside the schema text.
const SDLFileName = "gitlab-schema.graphql"
SDLFileName is the pinned schema's name on disk. The generator writes it and the package embeds it, so both sides name it once here.
The words are separated by a hyphen rather than an underscore because cmd/audit_doc_tool_names reads every gitlab_-prefixed identifier in the documentation as a tool name the server must register, and any page naming this file would otherwise fail that gate.
const SourceFileName = "source.json"
SourceFileName is the provenance record's name on disk, written by cmd/gen_graphql_schema beside the schema text.
Variables ¶
This section is empty.
Functions ¶
func Load ¶
Load parses an SDL document. It is exported so cmd/gen_graphql_schema can check a file it just wrote, or a committed one it did not, without going through the embedded copy.
func ParseAgainst ¶
ParseAgainst is ValidateDocumentAgainst returning what it parsed, so a caller can walk the fields, arguments and types a document actually depends on rather than the text it is written as.
The live re-probe compares two schemas that way. Whole-schema drift between two GitLab releases is thousands of lines and says nothing; the drift under our own selection sets is a handful of coordinates and says everything, and resolving those needs the validated document, where every field carries the definition it resolved to.
func ReleaseSchema ¶ added in v3.4.0
ReleaseSchema returns the schema of the released GitLab this server is held to, parsing the embedded SDL on first call.
It exists because the gitlab.com pin is a pre-release ahead of every self-managed instance. A field the pre-release adds passes every gate that reads that pin and is refused by every release, and GitLab refuses the whole document naming it: the re-pin of 2026-10-29 let crashState into the location selection seven security actions share, the gates stayed green, and GitLab 19.5.3 answered them with "Field 'crashState' doesn't exist on type 'VulnerabilityLocationCoverageFuzzing'". cmd/audit_graphql_documents judges every document against this schema as well as the pin, so a field the release lacks is a failure on the commit that selects it.
The returned schema is read-only and shared, as Schema's is.
func Schema ¶
Schema returns the pinned GitLab schema, parsing the embedded SDL on first call and returning the same schema to every later caller.
The returned schema is read-only. gqlparser's validator never mutates it, and callers must not either, because every goroutine in the process shares this one value.
func Validate ¶
Validate reports whether GitLab would accept document sent with variables.
It answers both halves of that question. The document half catches a field that does not exist, an argument the field does not accept and a variable used where its type does not fit. The variables half catches a variable sent that the operation never declared, which is the defect that let eight domains advertise backward pagination, and a value that does not fit the type it was declared as.
func ValidateDocument ¶
ValidateDocument reports whether the pinned schema accepts document, ignoring variable values. This is what a static audit can ask, since a document read out of the source has no request behind it.
func ValidateDocumentAgainst ¶
ValidateDocumentAgainst is ValidateDocument against a schema the caller loaded rather than the pinned one.
It exists for the live re-probe. The pin can only say a document was already broken on the day it was taken, never that GitLab has narrowed something since, which is how every defect this gate was built for arose: securityReportFindings accepted confidence once, and Project.vulnerabilities.severity was [String!] once. Handing the same check a schema fetched today turns that into a failure on the day it happens.
Types ¶
type ReleaseSource ¶ added in v3.4.0
type ReleaseSource struct {
// GitLabVersion is the release, spelled as the REST record spells its own
// version ("19.5.0-ee"). cmd/gen_graphql_schema -release --check holds the
// two to one another, which is what makes this the release the rest of the
// repository is pinned to rather than one somebody happened to fetch.
GitLabVersion string `json:"gitlab_version"`
// GitLabRevision is the commit the release's tag points at, which the REST
// record names as the revision its image reported.
GitLabRevision string `json:"gitlab_revision"`
// Origin is where the schema text was read from: the graphql-schema-dump
// job of the tag's pipeline, by the URL that names the job, or the file a
// re-pin was handed instead.
Origin string `json:"origin"`
// FromFile is true when Origin is a file a re-pin was handed (-from)
// rather than the dump of the release tag's job. A dump names no release
// of its own, so GitLabVersion and GitLabRevision are then the ones the
// REST record names, taken on its word: nothing checked that the file is
// the schema of that release.
FromFile bool `json:"from_file,omitempty"`
// SHA256 is of the schema text as it was read, before it was rendered
// into the form committed here, so a reader can compare it with the job's
// artifact without trusting the renderer.
SHA256 string `json:"sha256"`
// SDLSHA256 is of the rendered schema written beside this record, which
// the check recomputes from the file and holds the record to: a hand edit
// of the schema, or a regeneration that wrote one of the two files and
// not the other, fails it even where the type count still agrees.
SDLSHA256 string `json:"sdl_sha256"`
// RetrievedAt is the UTC day the schema text was read, as YYYY-MM-DD.
RetrievedAt string `json:"retrieved_at"`
// Types is how many types the schema beside this record loads with, which
// the check recomputes from the file and holds the record to, as it does
// for the gitlab.com pin.
Types int `json:"types"`
}
ReleaseSource describes where the release pin came from.
It is a record of its own rather than a Source because it answers other questions. A Source names an instance that was introspected, and the release pin is not taken from one: it is the schema GitLab's own CI dumped for the release's tag, so what a reader needs is which release, which commit and which job, and a digest of the bytes the job published.
func ParseReleaseSource ¶ added in v3.4.0
func ParseReleaseSource(record []byte) (ReleaseSource, error)
ParseReleaseSource decodes a release provenance record. It is exported so the generator can check a file on disk rather than the embedded copy.
func ReleaseSourceInfo ¶ added in v3.4.0
func ReleaseSourceInfo() (ReleaseSource, error)
ReleaseSourceInfo returns the provenance of the embedded release pin.
func (ReleaseSource) String ¶ added in v3.4.0
func (s ReleaseSource) String() string
String renders the provenance as one reportable line. A pin read from a file says so, since its release is the REST record's word rather than the tag's.
type Source ¶
type Source struct {
// Instance is the GraphQL endpoint that was introspected.
Instance string `json:"instance"`
// GitLabVersion is what that instance reported for itself, or "unknown"
// when the introspection ran without a token: GitLab answers introspection
// to anyone and refuses the metadata query to an anonymous caller.
GitLabVersion string `json:"gitlab_version"`
// GitLabRevision is the commit that instance was running, when known.
GitLabRevision string `json:"gitlab_revision,omitempty"`
// RetrievedAt is the UTC day the instance answered, as YYYY-MM-DD.
RetrievedAt string `json:"retrieved_at"`
// Types is how many types the schema beside this record loads with. It
// is the one number that says at a glance whether a regeneration got the
// whole schema or a truncated answer, and the loaded count rather than
// the introspected one because the check recomputes it from the file on
// disk and holds the record to it: the two files are one pin only while
// they agree.
Types int `json:"types"`
}
Source describes where the pinned schema came from, so a reader can tell how old the pin is without asking git. A schema that parses says nothing about whether the instance it was taken from still answers this way.
func ParseSource ¶
ParseSource decodes a provenance record. It is exported so the generator can check a file on disk rather than the embedded copy.
func SourceInfo ¶
SourceInfo returns the provenance of the embedded schema.
func (Source) String ¶
String renders the provenance as one reportable line.
The retrieval date is printed as the record spells it, which is a string field and not an instant: this line is a log and a gate's refusal message, read by whoever re-pins the schema, and the value they need is the one the committed record carries rather than a rendering of it.
type ValidationError ¶
type ValidationError struct {
// Reasons is one message per validation failure, in the order gqlparser
// reported them, followed by the variable failures this package adds.
Reasons []string
}
ValidationError carries every reason the pinned schema refused a document.
The reasons are kept apart rather than pre-joined because the two callers present them differently: the test transport prints one per line under the operation it came from, and the document audit indents them under the constant that declares the document.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
Error joins the reasons into one line.