Files
cozystack/internal/apigate/model.go
Timofei Larkin dec840e938 fix(ci): address review of the API gate
Fix schema-diff correctness: guard nil head on removed child schemas, flag adding a type to a previously-unconstrained field, stop reporting a head becoming unconstrained as narrowing, detect additionalProperties:false, and only recurse items/additionalProperties when both sides carry a schema.

Gate resource and group removal as new categories. Filter CRD versions to served ones so the deprecation flow surfaces as a removed version, and flag added x-kubernetes-validations (CEL) rules. Discover first-party cozystack.io CRDs by walking crds/ and definitions/ dirs so a relocated CRD (e.g. cozystack-scheduler) is covered, and error on an empty snapshot. Surface unmapped apiserver storage prefixes and allow hyphens in plurals; drop Markdown from text-mode reports.

Workflow: diff against the true merge base, count only each reviewer's latest review on the current head commit, and scope the token to the approval step with persist-credentials:false.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-07-01 14:51:33 +03:00

126 lines
4.7 KiB
Go

/*
Copyright 2026 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package apigate detects "sizeable" Cozystack API changes between two
// checkouts of the repository so CI can require a review from a designated
// API owner. Sizeable means one of: a new API group, a new resource, or a
// breaking change to an existing resource's schema. The detection is fully
// deterministic — it parses the checked-in, codegen-kept-in-sync artifacts
// that define the served API surface, with no network or LLM calls.
package apigate
// Category classifies why a change is sizeable. The three values mirror the
// gate's contract exactly.
type Category string
const (
// NewGroup is emitted when an API group appears in head but not base.
NewGroup Category = "new-api-group"
// NewResource is emitted when a (group, resource) pair appears in head
// but not base.
NewResource Category = "new-resource"
// Breaking is emitted when an existing resource's schema changes in a
// way that can reject requests or objects that the base schema accepted
// (removed field, narrowed type/enum, newly-required field, tightened
// constraint, removed served version).
Breaking Category = "breaking-change"
// RemovedGroup is emitted when an entire API group present in base is gone
// from head. Deleting a group withdraws its whole API surface.
RemovedGroup Category = "removed-api-group"
// RemovedResource is emitted when a (group, resource) present in base is
// gone from head — the most disruptive single-resource change, since every
// stored object and client of that resource breaks.
RemovedResource Category = "removed-resource"
)
// Source identifies which checked-in artifact a resource was parsed from.
// It is carried through to findings so the report points reviewers at the
// right file family.
type Source string
const (
// SourceCRD is a CustomResourceDefinition YAML manifest.
SourceCRD Source = "crd"
// SourceCozyRD is an ApplicationDefinition (cozyrd) backing the
// apps.cozystack.io aggregated API.
SourceCozyRD Source = "cozyrd"
// SourceAPIServer is a static Go-backed aggregated resource discovered
// from the apiserver storage registrations. These carry no checked-in
// schema, so they participate only in new-group / new-resource
// detection.
SourceAPIServer Source = "apiserver"
)
// Resource is the normalized identity + schema of one API resource, keyed
// across base and head by (Group, Plural). Plural is always present; Kind may
// be empty for static Go-backed resources whose kind is not recoverable from
// the storage registration alone.
type Resource struct {
Group string
Kind string
Plural string
Source Source
// Origin is the repo-relative path the resource was parsed from, used
// only for human-facing reporting.
Origin string
// Versions maps a served version name to its parsed schema. Empty for
// SourceAPIServer resources (schema lives only in Go structs, out of
// scope for breaking detection).
Versions map[string]Schema
}
// key is the base/head correlation key for a resource.
func (r Resource) key() resourceKey { return resourceKey{Group: r.Group, Plural: r.Plural} }
type resourceKey struct {
Group string
Plural string
}
// Schema is a parsed JSON/OpenAPI-v3 schema node, using the generic
// map[string]any shape produced by sigs.k8s.io/yaml (JSON number/string/bool
// semantics). Comparing parsed nodes rather than raw text makes the diff
// insensitive to key ordering and formatting, and lets us ignore purely
// descriptive fields.
type Schema map[string]any
// Snapshot is the full set of API resources discovered in one checkout,
// indexed by correlation key.
type Snapshot map[resourceKey]Resource
// groups returns the set of API group names present in the snapshot.
func (s Snapshot) groups() map[string]struct{} {
out := make(map[string]struct{})
for k := range s {
out[k.Group] = struct{}{}
}
return out
}
// Finding is one reason a change is sizeable, ready to render into the CI
// report.
type Finding struct {
Category Category
Group string
Kind string
Plural string
Source Source
Origin string
// Detail is a human-readable explanation. For Breaking findings it names
// the schema path and the nature of the break.
Detail string
}