platform

package
v0.4.62 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 12, 2026 License: MPL-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package platform is the CLI's client for the Plumber platform's three CI-OIDC endpoints: the project context read, the branch-aware config resolve, and (elsewhere) the result push.

The package is deliberately free of every other Plumber package. It carries the wire shapes and the transport, nothing else: the settings blobs inside a snapshot stay json.RawMessage here and are decoded by whichever provider package owns their types. That keeps configuration able to hold a *ProjectContext without an import cycle back through gitlab.

Decoding is FORWARD TOLERANT everywhere, as the platform's contract requires: unknown fields are ignored, and a field the CLI does not understand never fails a run.

Index

Constants

View Source
const (
	// ReasonResolverBusy means the platform's per-instance resolve
	// concurrency cap was saturated.
	ReasonResolverBusy = "resolver_busy"
	// ReasonResolutionUnavailable means the resolution could not be
	// performed: the timebox was exceeded, the git host was unreachable, no
	// org token is configured, or the project was never onboarded.
	ReasonResolutionUnavailable = "resolution_unavailable"
)

Unavailability reasons the resolve endpoint returns on a 503. Both are EXPECTED steady states, not errors to fail a run on.

View Source
const (
	DegradedFieldBranchProtection = "branch_protection"
	DegradedFieldMrApprovals      = "mr_approvals"
	DegradedFieldVariables        = "variables"
	DegradedFieldMergedYaml       = "merged_yaml"
	DegradedFieldProjectDetails   = "project_details"

	// DegradedFieldRawConfig means the raw CI config fetch itself
	// succeeded but exceeded the platform's size cap (maxRawConfigBytes,
	// 1 MiB) at collection time - a genuine could-not-serve-faithfully,
	// distinct from "could not be fetched" but the same not_evaluable
	// treatment applies.
	DegradedFieldRawConfig = "raw_config"
	// DegradedFieldSourceCatalog means at least one component include's
	// catalogue lookup genuinely failed. Never set when a lookup merely
	// determined the target is not a catalogue resource - that is a
	// present-but-empty answer, not a degradation.
	DegradedFieldSourceCatalog = "source_catalog"
	// DegradedFieldSecurityPolicyProject means the security-policy-project
	// linkage read was not authoritative (SecurityPolicyProject.Known is
	// false).
	DegradedFieldSecurityPolicyProject = "security_policy_project"
	// DegradedFieldIncludesJobs means at least one include's job
	// attribution could not be established, or the derive call was capped
	// or failed.
	DegradedFieldIncludesJobs = "includes_jobs"
)

The closed set of lane identifiers DegradedFields may carry. Anything else is a platform documentation bug; the CLI carries unknown values through to the operator rather than dropping them silently.

View Source
const DefaultTimeout = 15 * time.Second

DefaultTimeout bounds every platform call. The platform is a third party the pipeline must never be coupled to, so a slow or hung endpoint has to degrade rather than hold a CI job open.

View Source
const LocalDigestVersion = "1"

LocalDigestVersion is the digest_version this CLI computes under. It must equal the cidigest package's Version; the two are checked against each other in the wiring layer's tests rather than by importing cidigest here, which would give this package a dependency it does not otherwise need.

View Source
const NilUUID = "00000000-0000-0000-0000-000000000000"

NilUUID is the all-zero uuid the platform's derived "[Plumber default]" fallback policy carries. It is NOT a real policies row, so it must never be sent back as a push's policy_id. IsReal is the guard.

View Source
const SnapshotSchemaV2 = "2"

SnapshotSchemaV2 is the first schema version whose DegradedFields absence is a GUARANTEE rather than merely an absence.

Variables

This section is empty.

Functions

func IsUnavailable

func IsUnavailable(err error) (reason string, ok bool)

IsUnavailable reports whether err is a resolve-endpoint 503, and returns its reason. Callers use it to pick the not_evaluable reason to report.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to the platform's CI-OIDC endpoints. The zero value is not usable; build one with NewClient.

func NewClient

func NewClient(baseURL, token string) *Client

NewClient builds a client for baseURL authenticating with token, the CI OIDC id-token. Trailing slashes on baseURL are trimmed so callers may pass either spelling.

func (*Client) FetchContext

func (c *Client) FetchContext(projectPath string) (*ProjectContext, error)

FetchContext reads the project's resolved policy set and cached data snapshot. It is always a cache read on the platform side: the CLI never triggers a live collection.

The platform answers 200 for any authenticated, self-attributed project, INCLUDING one it has never seen (the policy set then falls back to the derived default). A 404 is therefore never an expected outcome here.

func (*Client) ResolveConfig

func (c *Client) ResolveConfig(projectPath, sha, digest, digestVersion string) (*ResolvedConfig, error)

ResolveConfig asks the platform to resolve the CI configuration at sha.

digest and digestVersion are the CLI's own computation and are sent as a pair; pass "" for both when the computation aborted, which tells the platform to resolve fresh rather than serve a cache entry keyed on a digest the CLI could not produce. Sending only one of the two is a 400, so this normalizes a half-supplied pair to neither.

A 503 comes back as *UnavailableError - an expected steady state (an un-onboarded project, a saturated resolver, an unreachable git host), not a reason to fail a run.

func (*Client) WithHTTPClient

func (c *Client) WithHTTPClient(h *http.Client) *Client

WithHTTPClient replaces the transport, for tests and for callers that need their own timeout.

type ConfigResolution

type ConfigResolution struct {
	// Source is the lane that supplied MergedYAML.
	Source ConfigSource

	// MergedYAML is the resolved CI configuration, empty when Source is
	// SourceUnavailable (and possibly empty when the git host reported the
	// config INVALID - see Valid).
	MergedYAML string

	// Digest is how the local computation compared to the anchor.
	Digest DigestStatus

	// LocalDigest / DigestVersion are what the CLI computed from its
	// checkout, empty when the computation aborted.
	LocalDigest   string
	DigestVersion string

	// DigestAbortReason is "overflow" or "read_failure" when Digest is
	// DigestNotComputed, so the operator learns which one happened.
	DigestAbortReason string

	// AnchorSha / AnchorRef / AnchorDigest are what the snapshot's config
	// was resolved against, carried for the verbose line even when the
	// digests diverged - an operator comparing two digests needs both.
	AnchorSha    string
	AnchorRef    string
	AnchorDigest string

	// ResolvedSha is the sha the platform resolved at, when Source is
	// SourceResolved. On a cache hit it may legitimately differ from the
	// sha that was requested.
	ResolvedSha string

	// FromCache reports whether a SourceResolved result was served from the
	// platform's cache rather than freshly resolved.
	FromCache bool

	// Includes is the per-include attribution the resolve endpoint served
	// for MergedYAML. It is nil when the config came from the snapshot
	// (whose own list the snapshot carries) or when the resolve endpoint
	// served no "includes" key at all (an older platform). It is non-nil,
	// possibly empty, when the endpoint DID serve the key: a served empty
	// list is a complete answer (this merged_yaml has zero includes), not
	// an unknown one, and must not be collapsed into the nil case. Read
	// through RunContext.Includes, which pairs each source with its
	// attribution and preserves this nil/non-nil distinction.
	Includes []json.RawMessage

	// Valid is false when the git host reported the CI config merge as
	// INVALID. That is a user error in their own config, not a resolution
	// failure, and MergedYAML may then be empty.
	Valid bool

	// Reason names why Source is SourceUnavailable, using the platform's
	// own vocabulary (ReasonResolutionUnavailable / ReasonResolverBusy) so
	// the not_evaluable findings it produces are machine-readable.
	Reason string

	// ShaFromAnchor records that the sha the resolve request asked about was
	// the snapshot anchor's, because the environment carried none (an
	// analyze outside CI). The run then evaluates the project's REMOTE state
	// at that commit, and the describe output says so: without the note, a
	// "digest diverges" line next to an anchor-sha resolution reads as a
	// contradiction to an operator whose divergence is local uncommitted
	// edits. Set by the caller that chose the sha, before any reader runs;
	// the resolving goroutine never touches it.
	ShaFromAnchor bool
	// contains filtered or unexported fields
}

ConfigResolution is the outcome of the whole branch-aware decision: which merged configuration this run evaluates against, and how it got there.

func StartRunConfigResolution

func StartRunConfigResolution(c resolver, snap Snapshot, projectPath, sha, localDigest, digestAbortReason string) *ConfigResolution

StartRunConfigResolution is ResolveRunConfig fired EARLY (#368): the digest comparison and every decision that needs no request settle before it returns, and on the one path that does need a request - a divergent digest with a client and a sha to ask about - the request is already in flight when it returns. The first accessor that needs the outcome joins on it, so the platform's resolve overlaps whatever runs in between instead of stalling the run up front; a hung endpoint costs its timeout in parallel with local work rather than before any of it starts.

Read outcome fields (Source, MergedYAML, ResolvedSha, FromCache, Valid, Reason) only after Settled() answers true or join() returns; the digest-side fields are final at return.

func (*ConfigResolution) Available

func (r *ConfigResolution) Available() bool

Available reports whether a merged configuration was obtained at all. Controls that read the merged config must report not_evaluable when this is false, never pass: an empty job list from an unavailable config is not evidence of a clean pipeline.

func (*ConfigResolution) Settled

func (r *ConfigResolution) Settled() bool

Settled reports whether the resolution's outcome fields are final. A resolution that never fired a request is settled from birth; one whose request is still in flight is not, and its outcome fields must not be read yet.

func (*ConfigResolution) SettledWithin

func (r *ConfigResolution) SettledWithin(d time.Duration) bool

SettledWithin waits up to d for the resolution to settle and reports whether it did. It exists for the one reader that wants the outcome if it is cheap but has something honest to print when it is not: the pre-run describe line.

type ConfigSource

type ConfigSource string

ConfigSource names where a run's merged CI configuration came from. It is reported in verbose output and decides which controls can be evaluated at all, so it is a fact about the run, not a diagnostic.

const (
	// SourceSnapshot: the checkout's config digest equals the snapshot's
	// resolution anchor, so this branch's config IS the one the platform
	// already resolved. The nominal path for every branch that does not
	// touch CI config, and it costs no extra call.
	SourceSnapshot ConfigSource = "snapshot"

	// SourceResolved: the digests diverged and the platform resolved this
	// branch's own config through its resolve endpoint.
	SourceResolved ConfigSource = "resolved"

	// SourceUnavailable: no merged configuration could be obtained. Controls
	// that depend on one report not_evaluable with Reason; every other
	// control still evaluates, and the run is never blocked by this.
	SourceUnavailable ConfigSource = "unavailable"
)

type DigestStatus

type DigestStatus string

DigestStatus records how the local digest compared to the snapshot's anchor. It exists to make the verbose line say WHY a branch took the path it did, rather than only which path.

const (
	// DigestMatch: local digest and version both equal the anchor's.
	DigestMatch DigestStatus = "match"

	// DigestDiverged: both sides produced a digest and they differ - this
	// branch really does change the CI config.
	DigestDiverged DigestStatus = "diverged"

	// DigestNoAnchor: the snapshot carries no digest to compare against
	// (the platform could not compute one, or there is no snapshot at all).
	// Treated as divergent: there is nothing to match.
	DigestNoAnchor DigestStatus = "no-anchor"

	// DigestNotComputed: the CLI's own computation aborted - a traversal
	// past the file cap, an unreadable file, or a CI config rooted in
	// another project. Treated as divergent, and the resolve request then
	// omits the digest pair entirely so the platform resolves fresh rather
	// than serving a cache entry keyed on a digest that was never produced.
	DigestNotComputed DigestStatus = "not-computed"
)

func CompareDigest

func CompareDigest(anchor *ResolutionAnchor, localDigest, localVersion string) DigestStatus

CompareDigest classifies a locally computed digest against the snapshot's anchor.

localDigest is "" when the CLI's own computation aborted; the caller passes the abort through rather than substituting a placeholder, because a missing digest and a differing digest lead to the same decision but must be reported differently.

func (DigestStatus) Divergent

func (d DigestStatus) Divergent() bool

Divergent reports whether this status means "do not reuse the snapshot's merged config". Only an exact match is non-divergent: every uncertain state - no anchor, no local digest - resolves rather than assumes, because assuming would evaluate a branch against the wrong configuration.

type DismissedIssue added in v0.4.59

type DismissedIssue struct {
	IdentityHash  string `json:"identity_hash"`
	RecipeVersion int    `json:"recipe_version"`
	ControlType   string `json:"control_type"`
}

DismissedIssue is one entry of ProjectContext.DismissedIssues: the match key for a finding the platform has in status Dismissed (#447). IdentityHash is the platform's full sha256 hex over the shared recipe's Pairs() (identity.PlatformHash); RecipeVersion is the recipe the hash was computed under and versions never mix; ControlType rides along as a cheap pre-filter, never part of the key.

type Enforcement

type Enforcement string

Enforcement is a policy's enforcement dial. The platform's vocabulary is closed today ("block" | "report") but is carried as a string so an unrecognized future value degrades to "not blocking" rather than failing the decode.

const (
	// EnforcementBlock means a failing verdict for this policy should block.
	EnforcementBlock Enforcement = "block"
	// EnforcementReport means findings are recorded but never block.
	EnforcementReport Enforcement = "report"
)

func (Enforcement) Blocking

func (e Enforcement) Blocking() bool

Blocking reports whether this dial blocks a pipeline. Anything other than the exact "block" value is treated as report-only: an unknown dial must never be guessed into blocking someone's pipeline.

type Policy

type Policy struct {
	ID           string              `json:"id"`
	Name         string              `json:"name"`
	Enforcement  Enforcement         `json:"enforcement"`
	Requirements []PolicyRequirement `json:"requirements,omitempty"`

	// MinPoints is the policy's enforcement threshold as the platform serves it
	// (Policy.min_points on /context, nullable): a block-mode policy blocks when
	// its recomputed FINAL points fall strictly below it. nil = unset, the
	// platform's any-fail rule applies. Read by the platform-verdict renderer
	// only; never a gate the CLI evaluates itself (the exit code is the
	// platform's verdict).
	MinPoints *int `json:"min_points,omitempty"`
}

Policy is one entry of the resolved policy set. The set is never empty: an unassigned project resolves to the derived "[Plumber default]" fallback, report-only, carrying NilUUID.

Requirements is this policy's OWN control tree. Two different policies may declare the same control_type with DIFFERENT config, which is the whole point of the field: a control's parameters belong to the policy that declared them and must never be read from a sibling policy.

It is empty for the derived "[Plumber default]" fallback, which is not a real policies row and therefore has no tree to read. An empty tree is an honest statement that the policy configures nothing, never an error.

func (Policy) ControlConfig

func (p Policy) ControlConfig(controlType string) (json.RawMessage, bool)

ControlConfig returns the raw config this policy declares for a control type, and whether the policy declares it at all. The search is over this policy's own tree only - never a sibling's.

func (Policy) DeclaresAnyControl

func (p Policy) DeclaresAnyControl() bool

DeclaresAnyControl reports whether this policy carries a non-empty tree. A policy that declares nothing must fall back to the CLI's local config rather than evaluate against an empty ruleset.

func (Policy) IsReal

func (p Policy) IsReal() bool

IsReal reports whether this policy has a real platform id that a push may be keyed on. The derived fallback carries the nil uuid and must be pushed name-only instead, so a result is never keyed to a row that does not exist.

type PolicyControl

type PolicyControl struct {
	ControlType string          `json:"control_type"`
	Config      json.RawMessage `json:"config"`
}

PolicyControl is one control instance a requirement declares.

Config stays RAW deliberately. The platform serves the bytes its own policy_controls.config column stores, and decoding into a generic map and re-marshaling was measured on the platform side to corrupt integers above 2^53 and to reorder keys. Carrying the bytes through untouched is what makes "verbatim" true rather than merely "semantically equivalent".

type PolicyRequirement

type PolicyRequirement struct {
	Name     string          `json:"name"`
	Controls []PolicyControl `json:"controls"`
}

PolicyRequirement is one requirement grouping inside a policy's tree. Order is the platform's own stored position and is stable across requests; readers should not re-sort it.

type ProjectContext

type ProjectContext struct {
	SchemaVersion   int              `json:"schema_version"`
	Project         string           `json:"project"`
	Policies        []Policy         `json:"policies"`
	Snapshot        Snapshot         `json:"snapshot"`
	DismissedIssues []DismissedIssue `json:"dismissed_issues"`
}

ProjectContext is the GET .../context response: the resolved policy set plus the cached data snapshot. Fetching it never triggers a collection on the platform - it is always a cache read.

type ProjectDetails added in v0.4.57

type ProjectDetails struct {
	// DefaultBranch is the project's default branch at collection time.
	DefaultBranch string `json:"default_branch"`
	// Archived reports whether the project is archived. An archived
	// project still gets this section; its merged_yaml is honest-empty
	// with no degraded_fields entry.
	Archived bool `json:"archived"`
	// PathWithNamespace is the project's full path (group/project) as
	// GitLab reports it.
	PathWithNamespace string `json:"path_with_namespace"`

	// MergeMethod is GitLab's merge_method setting ("merge", "rebase_merge"
	// or "ff"). Nil on a pre-2026-08-28 snapshot.
	MergeMethod *string `json:"merge_method,omitempty"`
	// SquashOption is GitLab's squash_option setting. Nil on a
	// pre-2026-08-28 snapshot.
	SquashOption *string `json:"squash_option,omitempty"`
	// MergePipelinesEnabled is GitLab's merge_pipelines_enabled setting.
	// Nil on a pre-2026-08-28 snapshot.
	MergePipelinesEnabled *bool `json:"merge_pipelines_enabled,omitempty"`
	// MergeTrainsEnabled is GitLab's merge_trains_enabled setting. Nil on a
	// pre-2026-08-28 snapshot.
	MergeTrainsEnabled *bool `json:"merge_trains_enabled,omitempty"`
	// AllowMergeOnSkippedPipeline is GitLab's
	// allow_merge_on_skipped_pipeline setting. Nil on a pre-2026-08-28
	// snapshot.
	AllowMergeOnSkippedPipeline *bool `json:"allow_merge_on_skipped_pipeline,omitempty"`
	// ResolveOutdatedDiffDiscussions is GitLab's
	// resolve_outdated_diff_discussions setting. Nil on a pre-2026-08-28
	// snapshot.
	ResolveOutdatedDiffDiscussions *bool `json:"resolve_outdated_diff_discussions,omitempty"`
	// PrintingMergeRequestLinkEnabled is GitLab's
	// printing_merge_request_link_enabled setting. Nil on a pre-2026-08-28
	// snapshot.
	PrintingMergeRequestLinkEnabled *bool `json:"printing_merge_request_link_enabled,omitempty"`
	// RemoveSourceBranchAfterMerge is GitLab's
	// remove_source_branch_after_merge setting. Nil on a pre-2026-08-28
	// snapshot.
	RemoveSourceBranchAfterMerge *bool `json:"remove_source_branch_after_merge,omitempty"`
}

ProjectDetails is the project's core facts from the same lookup that resolves CiConfigPath (2026-08-27, #368 ask 4). Present whenever that lookup succeeds, archived projects included: the archived flag is half the point.

The eight merge-settings fields below (MergeMethod through RemoveSourceBranchAfterMerge) were added a day later (2026-08-28, #368 tier c) and are pointers ON PURPOSE: they are always present on a fresh collection since that date but ABSENT on a snapshot stored before then (it self-heals on the next refresh). A non-pointer decode would fabricate a false or empty value for that stale blob, which is exactly the "degraded stays honestly absent" rule SnapshotData exists to uphold.

type ResolutionAnchor

type ResolutionAnchor struct {
	Ref           string `json:"ref"`
	Sha           string `json:"sha"`
	ConfigDigest  string `json:"config_digest,omitempty"`
	DigestVersion string `json:"digest_version,omitempty"`
}

ResolutionAnchor records what a snapshot's merged_yaml was resolved against. Ref and Sha are always present together. ConfigDigest and DigestVersion are present only when the platform could compute them, and are absent TOGETHER - an honest absence, never a fabricated value.

func (*ResolutionAnchor) HasDigest

func (a *ResolutionAnchor) HasDigest() bool

HasDigest reports whether this anchor carries a usable comparison key. An anchor without one is ALWAYS DIVERGENT: there is nothing to compare against, so the branch's config must be resolved rather than assumed to match.

func (*ResolutionAnchor) Matches

func (a *ResolutionAnchor) Matches(digest, version string) bool

Matches reports whether a locally computed (digest, version) pair is the same resolved config this anchor describes. Both the digest and the version must match: digests computed under different versions are not comparable even when both are 64 hex characters.

type ResolveRequest

type ResolveRequest struct {
	Sha           string `json:"sha"`
	ConfigDigest  string `json:"config_digest,omitempty"`
	DigestVersion string `json:"digest_version,omitempty"`
}

ResolveRequest asks the platform to resolve the CI config at a specific sha. ConfigDigest and DigestVersion are an ALL-OR-NOTHING pair and are omitted together when the CLI's own digest computation aborted (a traversal over the file cap, or a read failure); the platform then treats the config as uncacheable and resolves fresh.

type ResolvedConfig

type ResolvedConfig struct {
	// MergedYaml is the resolved configuration. It may be EMPTY when Valid
	// is false.
	MergedYaml string `json:"merged_yaml"`

	// ResolvedSha is the sha this merged_yaml was resolved at. On a cache
	// hit it is the CACHED resolution's sha, which may differ from the sha
	// that was requested: identical config content shares one resolution.
	// Never assert it equals the requested sha.
	ResolvedSha string `json:"resolved_sha"`

	// Valid is false when the git host reported the CI config merge as
	// INVALID - a user error in the pushed config, not a resolution
	// failure. Still a 200.
	Valid bool `json:"valid"`

	// Source is "cache" or "resolved".
	Source string `json:"source"`

	// ConfigDigest / DigestVersion are the PLATFORM's own computation and
	// are authoritative for its cache key. A mismatch against what the CLI
	// sent is a diagnostic, never an error.
	ConfigDigest  string `json:"config_digest,omitempty"`
	DigestVersion string `json:"digest_version,omitempty"`

	// Includes is the per-include attribution for THIS merged_yaml, in the
	// same per-entry shape as snapshot.data.includes. Left raw for the same
	// reason the snapshot's list is: the provider package owns that type,
	// and a field added upstream must travel through untouched.
	//
	// PRESENCE is the fact that matters, not just content, and the plain
	// slice already carries it through encoding/json's own decode rules: a
	// body with no "includes" key leaves this nil (Go never touches a
	// field it did not find), while a body with "includes": [] decodes to
	// a non-nil, zero-length slice (encoding/json replaces the field with
	// a fresh empty slice for an empty JSON array). So:
	//
	//   - nil: absent on an older platform, or the endpoint genuinely has
	//     nothing to say about this document. Not emptiness - a run with no
	//     attribution reports the include controls not_evaluable rather
	//     than reading the config as having no includes.
	//   - non-nil, len 0: served and complete - the platform positively
	//     knows this merged_yaml has zero includes, which is a real answer
	//     ("every job here is project-authored"), not an unknown one.
	//   - non-nil, len > 0: served attribution for each include.
	//
	// Callers must read this field's nil-ness, never its length, to tell
	// "unknown" apart from "known empty" - see RunContext.Includes.
	Includes []json.RawMessage `json:"includes,omitempty"`
}

ResolvedConfig is the 200 response of the resolve endpoint.

type RunContext

type RunContext struct {
	// Endpoint is the platform base URL this run is bound to.
	Endpoint string

	// ProjectPath is the path the context was fetched for.
	ProjectPath string

	// Context is the fetched /context response. Nil when the fetch itself
	// failed, in which case platform mode degrades to a name-only push and
	// the standalone collection lanes.
	Context *ProjectContext

	// Config is the branch-aware merged-config decision for this run.
	Config *ConfigResolution

	// ContextErr records why Context is nil, for the operator-facing line.
	ContextErr error
	// contains filtered or unexported fields
}

RunContext is everything platform mode resolved BEFORE collection began: the project's policy set, the cached settings snapshot, and which merged CI configuration this run evaluates against.

A nil *RunContext means standalone mode - the CLI's default and unchanged behaviour. Every consumer must treat nil as "no platform", never as an error, so the standalone path stays exactly what it is today.

func (*RunContext) Active

func (r *RunContext) Active() bool

Active reports whether --platform was set for this run.

func (*RunContext) Client

func (r *RunContext) Client() *Client

Client returns the authenticated client, or nil when platform mode never established one.

func (*RunContext) ConfigAndIncludesAgree

func (r *RunContext) ConfigAndIncludesAgree() bool

ConfigAndIncludesAgree reports whether the configuration in use has attribution that describes IT. It is the question the include-reasoning controls ask, and it is answered by Includes alone: the two are served together or not at all.

func (*RunContext) ConfigInvalid

func (r *RunContext) ConfigInvalid() bool

ConfigInvalid reports whether the git host judged the configuration this run evaluates to be INVALID - a user error in their own CI file, not a resolution failure.

The two must not be collapsed, and the direction matters. An unavailable resolution says nothing about the user's config and must never be reported as "your CI file is broken". An INVALID one is a real answer about it, and reporting it as valid is worse: the merge is partial or empty, so the jobs that failed to merge are simply absent and every pipeline control passes over what is left. A run whose config does not merge would print a clean green verdict.

func (*RunContext) ConfigIsSnapshot added in v0.4.62

func (r *RunContext) ConfigIsSnapshot() bool

ConfigIsSnapshot reports whether the document this run evaluates IS the snapshot's own merged_yaml.

It is a narrower question than ConfigAndIncludesAgree and must not be collapsed into it. Everything else the snapshot says ABOUT its document - the git host's merge verdict above all - applies only to that document. A resolve-sourced config can now arrive with attribution of its own, which makes attribution available without making the snapshot's other verdicts apply; it has its own, through ConfigInvalid.

func (*RunContext) DegradationKnowable

func (r *RunContext) DegradationKnowable() bool

DegradationKnowable reports whether this run can distinguish an honestly empty lane from a failed one. False on a pre-v2 snapshot, where absence proves nothing.

func (*RunContext) DegradedLanes

func (r *RunContext) DegradedLanes() []string

DegradedLanes returns the failed lanes in a stable order, for the operator-facing summary and the push metadata. Empty when the snapshot cannot tell us (below schema v2), which Describe reports distinctly from "nothing degraded".

func (*RunContext) Describe

func (r *RunContext) Describe() []string

Describe renders the operator-facing summary of what platform mode resolved: the policy set, the snapshot's age, and which configuration this run is evaluating against and why. Returned as lines so the caller controls prefixing and the whole thing stays testable as data.

Every line states a fact the run actually observed. Nothing here is inferred or defaulted into looking healthier than it is.

func (*RunContext) Engaged

func (r *RunContext) Engaged() bool

Engaged reports whether platform mode actually took over this run's data lanes, which requires a context the CLI genuinely fetched.

The distinction matters when the platform could not be reached at all. The lane split is a division of labour agreed with a platform that ANSWERED: it says "do not collect this yourself, the platform has it". With no answer there is no such agreement, and treating the lanes as assigned anyway would blank an entire scan because a third party was briefly down. Such a run falls back to collecting locally, and says so — see Describe, which reports the fetch failure and its cause on every run, verbose or not.

This is not a way to prefer the local lane when it is convenient: once the platform answers, its lanes apply even when what it answered with is "no configuration available".

func (*RunContext) Includes added in v0.4.62

func (r *RunContext) Includes() ([]json.RawMessage, bool)

Includes returns the per-include attribution for the configuration this run actually evaluates, and whether any is available.

Attribution and merged document come from two lanes, and each document has exactly one list that describes it:

  • SourceSnapshot: MergedYAML is the snapshot's own merged_yaml, so the snapshot's includes attribute exactly it.
  • SourceResolved: MergedYAML is what the platform resolved for THIS branch, so only the includes the resolve response carried describe it. The endpoint serves them when it can; when it does not, there is no branch-accurate attribution to be had and this returns false.

ok is keyed on PRESENCE, not on how many entries came back. A source that served the key with zero entries is a complete answer - every job in the merged document is project-authored, nothing to attribute upstream - and returns ok=true with a non-nil, empty list. Only an ABSENT key (the source never served attribution at all) returns ok=false. Collapsing "served empty" into "not served" would abstain the include-reasoning controls on a project that legitimately has no includes.

The snapshot's list is never lent to a resolved config. It was resolved against the anchor, the default branch, and a branch that adds, removes or re-pins an include would get every job from that include classified against the OLD attribution: upstream jobs read as project-authored and vice versa. That is the fabricated-finding mode attribution exists to prevent, landing on precisely the divergent branch the digest exists to detect. Callers must treat attribution as unavailable when ok is false.

func (*RunContext) LaneDegraded

func (r *RunContext) LaneDegraded(field string) bool

LaneDegraded reports whether a named snapshot lane failed collection on the platform side. False when there is no snapshot, or when the payload predates the bookkeeping - see SnapshotData.DegradedFieldsTrusted.

func (*RunContext) LaneMissing

func (r *RunContext) LaneMissing(field string) bool

LaneMissing reports whether a named snapshot lane carried no data at all.

It is a different question from LaneDegraded, and for some lanes it is the more important one. The platform writes branch_protection and mr_approvals on ANY successful collection, empty lists included, so their absence is never "this project has none" - it only ever means the collection did not complete. A caller reading an absent lane as an empty one would certify an unprotected default branch as compliant.

Other lanes are the opposite: variables is omitted when the project genuinely has none, which is a real answer both variable controls should pass on. Which reading applies is the LANE's property, so it is decided by the caller that knows the lane, not here.

func (*RunContext) MergedYAML

func (r *RunContext) MergedYAML() (string, bool)

MergedYAML returns the resolved CI configuration for this run, and whether one is available at all. A false second return means controls reading the merged configuration must report not_evaluable.

func (*RunContext) MissingSnapshotFields

func (r *RunContext) MissingSnapshotFields() []string

MissingSnapshotFields lists the snapshot lanes that carried no data, in a stable order. It feeds the push's collection.missing_fields, which is the platform's honest-degradation signal: a lane that was never collected is reported as missing rather than silently read as empty.

func (*RunContext) Policies

func (r *RunContext) Policies() []Policy

Policies returns the resolved policy set, or nil when no context was fetched. The platform guarantees the set is never empty when a context WAS fetched: an unassigned project resolves to the derived default.

func (*RunContext) SetClient

func (r *RunContext) SetClient(c *Client)

SetClient attaches the authenticated client, so the later result push reuses this run's token instead of minting a second one.

func (*RunContext) Snapshot

func (r *RunContext) Snapshot() Snapshot

Snapshot returns the cached settings snapshot, or a zero Snapshot when none was fetched. The zero value is honestly empty (no CollectedAt, no Data), which every reader already handles.

func (*RunContext) SnapshotCIConfigPath

func (r *RunContext) SnapshotCIConfigPath() string

SnapshotCIConfigPath returns the project's configured CI config path from the snapshot, or "" when the platform did not supply one. The caller decides the default; this reports only what was served.

func (*RunContext) SnapshotCollectedAt

func (r *RunContext) SnapshotCollectedAt() string

SnapshotCollectedAt returns the snapshot's collection time in RFC3339, or "" when there is no snapshot. It is carried on the push so the platform records which cache read a verdict was computed from.

func (*RunContext) SnapshotIncludes

func (r *RunContext) SnapshotIncludes() ([]json.RawMessage, bool)

SnapshotIncludes returns the snapshot's per-include attribution, and whether the platform supplied any. It describes the SNAPSHOT's document, so callers evaluating a configuration want Includes, which pairs each source with the attribution that belongs to it. Without attribution a component's job is indistinguishable from one the project wrote, which fabricates findings rather than merely hiding them.

ok answers whether the snapshot carries the key at all, not whether it carries any entries: a snapshot served with includes: [] is a complete, known-empty answer and returns ok=true with a non-nil empty list. Only a snapshot with no Data, or one whose Includes key was never served (nil), returns ok=false.

func (*RunContext) SnapshotMergeVerdict added in v0.4.57

func (r *RunContext) SnapshotMergeVerdict() (status string, errs []string, served bool)

SnapshotMergeVerdict returns the git host's OWN answer about the snapshot's merged_yaml - its status ("VALID" / "INVALID") and the merge errors that go with it - and whether the platform served either.

It is the only place a snapshot-path run can learn that verdict. StartRunConfigResolution starts every resolution Valid and never clears the flag when the merged configuration comes from the snapshot, so a caller synthesizing the status locally can only ever say VALID there: a snapshot whose merge GitLab itself rejected reads as a clean config, with the jobs that failed to merge simply absent and every control passing over what is left.

The verdict describes the SNAPSHOT's document. A caller evaluating a different one (a digest-divergent branch, resolved by the platform for this run) must not attach it - see RunContext.ConfigIsSnapshot, which decides exactly that question.

The error slice is copied: the snapshot is shared for the whole run and a caller that appends to what it is handed would edit it.

func (*RunContext) SnapshotRawConfig added in v0.4.57

func (r *RunContext) SnapshotRawConfig() (string, bool)

SnapshotRawConfig returns the project's own UNMERGED root CI file as the platform served it, and whether it may be used.

It closes the gap a run with no checkout of the analyzed project has: the merged pipeline comes from the platform, but the pre-merge document pipelineMustNotOverrideJobVariables compares against is unreadable, and that control fails silently without it rather than loudly.

A false second return keeps that honest gap rather than closing it with something weaker. Three states produce one:

  • The lane is DEGRADED: the platform fetched the file and could not serve it faithfully (its own size cap), so what is on offer is a truncation, and an incomplete root file yields fewer hardcoded jobs and fewer overridden variables - a silent pass, the one direction the abstention exists to prevent.
  • Nothing was served at all.
  • The configuration under evaluation is not the one the file was collected beside: see anchorCoversEvaluatedConfig.

func (*RunContext) UnavailableReason

func (r *RunContext) UnavailableReason() string

UnavailableReason names why no merged configuration is available, or "" when one is. It is the reason stamped onto the not_evaluable findings this state produces.

type SecurityPolicyProject added in v0.4.57

type SecurityPolicyProject struct {
	Known    bool    `json:"known"`
	ID       *int    `json:"id,omitempty"`
	FullPath *string `json:"full_path,omitempty"`
}

SecurityPolicyProject is the GitLab security-policy-project linkage (2026-08-27, #368 ask 6).

Known true with ID and FullPath set means the project is linked. Known true ALONE (both pointers nil) means the linkage was read authoritatively and genuinely found nothing linked - the real Critical for ISSUE-601, not an absence to be confused with "could not check". Known false means the read was NOT authoritative (auth failure, a null GraphQL project, or the field being unavailable on this instance), always paired with the "security_policy_project" degraded_fields entry; callers must treat that as not_evaluable, never as a pass or a fail.

ID and FullPath are pointers because they are omitted, not zeroed, when there is nothing to report: ID absent means either not linked or not known, never a real id of 0.

type Snapshot

type Snapshot struct {
	CollectedAt *time.Time    `json:"collected_at,omitempty"`
	Data        *SnapshotData `json:"data,omitempty"`
}

Snapshot is the platform's cached data collection for a project. A cache miss serializes as an empty snapshot (both fields nil) on a 200 - never a 404 and never fabricated content.

func (Snapshot) Anchor

func (s Snapshot) Anchor() *ResolutionAnchor

Anchor returns the snapshot's resolution anchor, or nil when the snapshot carries no data or was collected without one.

type SnapshotData

type SnapshotData struct {
	// SchemaVersion tags this payload's shape. Absent on snapshots
	// collected before the field existed - an honest absence, not a "1".
	SchemaVersion string `json:"schema_version,omitempty"`

	// BranchProtection is {"protections": [...]} in the platform's current
	// shape. Raw: see the type doc.
	BranchProtection json.RawMessage `json:"branch_protection,omitempty"`

	// MergedYaml is the resolved CI configuration at ResolutionAnchor's sha.
	MergedYaml string `json:"merged_yaml,omitempty"`

	// MrApprovals is {"rules": [...], "settings": {...}}. Raw: see the type doc.
	MrApprovals json.RawMessage `json:"mr_approvals,omitempty"`

	// ResolutionAnchor is present only alongside a successfully resolved
	// MergedYaml.
	ResolutionAnchor *ResolutionAnchor `json:"resolution_anchor,omitempty"`

	// Variables is CI/CD variable METADATA - names, types, scopes and the
	// protected/masked/hidden flags. The platform never serves variable
	// VALUES here, by design. Raw: see the type doc.
	Variables json.RawMessage `json:"variables,omitempty"`

	// Includes is the git host's own per-include attribution for MergedYaml,
	// carried AS-IS in the CLI's MergedCIConfResponseInclude shape. It is the
	// difference between knowing a job came from an upstream component and
	// guessing the project wrote it; see control.controlsRequiringIncludeAttribution
	// for what depends on it. Present only alongside a resolved MergedYaml,
	// the same presence rule as ResolutionAnchor. Raw: decoded by gitlab.
	//
	// Carries the same nil-vs-non-nil presence distinction as
	// ResolvedConfig.Includes, for the same reason: a snapshot collected
	// with the key absent decodes nil (unknown), one collected with
	// "includes": [] decodes to a non-nil, zero-length slice (known, zero
	// includes - a complete answer, not an unknown one). Callers must read
	// nil-ness, never length, to tell the two apart - see
	// RunContext.SnapshotIncludes.
	Includes []json.RawMessage `json:"includes,omitempty"`

	// CiConfigPath is the project's OWN configured CI config path, defaulting
	// to ".gitlab-ci.yml". The platform computes its anchor digest against
	// this path, so a project with a custom path can only ever cache-hit if
	// the CLI digests against it too.
	CiConfigPath string `json:"ci_config_path,omitempty"`

	// ProjectDetails carries the project's core facts and, when the
	// collection ran on or after 2026-08-28, its eight merge settings - see
	// the ProjectDetails type doc. Nil when the underlying lookup failed
	// (paired with DegradedFieldProjectDetails) or (pre-2026-08-27) the
	// platform had not started serving it yet.
	ProjectDetails *ProjectDetails `json:"project_details,omitempty"`

	// SecurityPolicyProject is the GitLab security-policy-project linkage -
	// see the SecurityPolicyProject type doc for its three meaningful
	// shapes. Nil when the linkage was never read (pre-2026-08-27
	// platform, or the field simply was not served).
	SecurityPolicyProject *SecurityPolicyProject `json:"security_policy_project,omitempty"`

	// RawConfig is the RAW, un-merged root CI config file exactly as
	// fetched, at CiConfigPath on the default branch. Present whenever the
	// file fetch itself succeeded, including when the merge step failed or
	// reported INVALID - so a reader can distinguish "no CI file" from
	// "file exists but the merge is absent/broken". Absent past the
	// platform's own size cap (paired with DegradedFieldRawConfig) or when
	// the fetch genuinely failed.
	RawConfig string `json:"raw_config,omitempty"`

	// MergedYamlStatus is GitLab's own merge status for MergedYaml,
	// verbatim ("VALID" or "INVALID"). Absent when the merge response
	// itself is absent: an archived project (see ProjectDetails.Archived)
	// or the merge fetch failed (then DegradedFieldMergedYaml is set).
	// Deliberately not named "Status" bare (INVARIANTS rule A).
	MergedYamlStatus string `json:"merged_yaml_status,omitempty"`

	// CiErrors is GitLab's CI lint/merge errors for MergedYaml, verbatim.
	// Omitted when empty; typically non-empty exactly when
	// MergedYamlStatus is "INVALID".
	CiErrors []string `json:"ci_errors,omitempty"`

	// DegradedFields names the collection lanes that FAILED for this
	// snapshot, from the closed set in DegradedField*. It is the distinction
	// the CLI could not previously make: a lane that is absent AND unlisted
	// is honestly empty and a control may FAIL against it, while a listed
	// lane could not be read and must report not_evaluable instead.
	//
	// Trust it only at SchemaVersion "2" or later - see DegradedFieldsTrusted.
	DegradedFields []string `json:"degraded_fields,omitempty"`
}

SnapshotData is the collected project settings the platform serves from its own cache. Every field is optional: a collection that degraded stays honestly absent rather than being fabricated as a zero value.

BranchProtection, MrApprovals and Variables stay raw here on purpose - their shapes are the GitLab provider's, and decoding them in this package would drag that dependency in. See gitlab.ProtectionFromSnapshot.

func (*SnapshotData) DegradedFieldsTrusted

func (d *SnapshotData) DegradedFieldsTrusted() bool

DegradedFieldsTrusted reports whether this payload's DegradedFields may be read as complete. Below v2 the field did not exist, so an empty list is indistinguishable from an older collection that never recorded the bookkeeping - treating that as "nothing degraded" is exactly the false reassurance the version gate exists to prevent. The comparison is NUMERIC, not lexical. Comparing the strings would make "10" sort below "2" and silently distrust every snapshot from schema 10 onward - a bug that would lie dormant for years and then quietly disable the degradation signal exactly when it is hardest to notice.

A version that is not a number at all is not trusted: an unparseable tag is not evidence the bookkeeping exists.

func (*SnapshotData) IsDegraded

func (d *SnapshotData) IsDegraded(field string) bool

IsDegraded reports whether a named lane failed collection. It answers false when the payload predates the bookkeeping (see DegradedFieldsTrusted) - callers must not read that false as "this lane is fine", only as "this snapshot cannot tell us".

type StatusError

type StatusError struct {
	StatusCode int
	Status     string
	Body       string
}

StatusError reports a non-2xx response that is not a handled 503.

func (*StatusError) Error

func (e *StatusError) Error() string

type UnavailableError

type UnavailableError struct {
	// Reason is the platform's machine-readable vocabulary. It falls back
	// to ReasonResolutionUnavailable when the body carried none, so callers
	// always have a reason to report.
	Reason string
}

UnavailableError reports a 503 from the resolve endpoint. It is a normal outcome, not a failure: the caller degrades the controls that depend on a resolved config to not_evaluable and lets everything else run.

func (*UnavailableError) Error

func (e *UnavailableError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL