Skip to content

Commit 1acf128

Browse files
authored
ci(sdk): gate client API compatibility (NVIDIA#2060)
Signed-off-by: Tjark Gunnar Rasche <trasche@nvidia.com>
1 parent da5dd5b commit 1acf128

18 files changed

Lines changed: 1015 additions & 9 deletions

File tree

.github/actions/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ executable bits or `./script.sh` invocation.
1313

1414
### Core CI/CD Actions
1515

16+
#### `go-test/`
17+
18+
**Purpose**: Set up Go and Helm, verify vendored dependencies, and run unit tests with race detection and coverage
19+
**When to use**: Go CI workflows that use the repository's `make test` target
20+
**Inputs**:
21+
- `go_version` (required): Go version to install
22+
- `coverage_report` (optional): Whether to generate a coverage report (default: "false")
23+
- `coverage_threshold` (optional): Minimum coverage percentage (default: empty)
24+
- `helm_version` (required): Helm version from `load-versions`
25+
- `apidiff_version` (optional): apidiff version from `load-versions`; when set, installs apidiff and runs `make api-diff` (default: empty, which skips both steps)
26+
27+
Callers that set `apidiff_version` must check out full history with
28+
`fetch-depth: 0` so `make api-diff` can resolve a reachable stable release tag.
29+
1630
#### `security-scan/`
1731
**Purpose**: Anchore/Grype vulnerability scanning with SARIF upload
1832
**When to use**: Security validation in CI/CD pipelines
@@ -339,12 +353,15 @@ jobs:
339353
runs-on: ubuntu-latest
340354
steps:
341355
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
356+
with:
357+
fetch-depth: 0
342358
- uses: ./.github/actions/load-versions
343359
id: versions
344360
- uses: ./.github/actions/go-test
345361
with:
346362
go_version: ${{ steps.versions.outputs.go }}
347363
helm_version: ${{ steps.versions.outputs.helm }}
364+
apidiff_version: ${{ steps.versions.outputs.apidiff }}
348365
coverage_report: 'true'
349366
- uses: ./.github/actions/go-lint
350367
with:
@@ -360,12 +377,15 @@ jobs:
360377
runs-on: ubuntu-latest
361378
steps:
362379
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
380+
with:
381+
fetch-depth: 0
363382
- uses: ./.github/actions/load-versions
364383
id: versions
365384
- uses: ./.github/actions/go-test
366385
with:
367386
go_version: ${{ steps.versions.outputs.go }}
368387
helm_version: ${{ steps.versions.outputs.helm }}
388+
apidiff_version: ${{ steps.versions.outputs.apidiff }}
369389
- uses: ./.github/actions/go-build-release
370390
id: release
371391
with:
@@ -437,3 +457,7 @@ To use these actions in other repositories:
437457
helm_version: 'v4.2.2'
438458
coverage_report: 'true'
439459
```
460+
461+
The cross-repository example intentionally omits `apidiff_version`. Repositories
462+
without AICR's `make api-diff` target retain the original test behavior because
463+
an empty `apidiff_version` skips the API compatibility steps.

.github/actions/go-test/action.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ inputs:
3030
helm_version:
3131
description: 'Helm version pinned in .settings.yaml (testing_tools.helm, via load-versions) — required by the argocd-helm live-render tests, which fail (not skip) in CI when helm is missing'
3232
required: true
33+
apidiff_version:
34+
description: 'Optional apidiff version pinned in .settings.yaml (linting.apidiff, via load-versions); leave empty to skip the SDK API compatibility check'
35+
required: false
36+
default: ''
3337

3438
runs:
3539
using: 'composite'
@@ -71,9 +75,23 @@ runs:
7175
shell: bash
7276
run: make test
7377

78+
- name: Install API-diff tool
79+
if: inputs.apidiff_version != ''
80+
shell: bash
81+
env:
82+
APIDIFF_VERSION: ${{ inputs.apidiff_version }}
83+
run: |
84+
set -euo pipefail
85+
GOFLAGS= go install "golang.org/x/exp/cmd/apidiff@${APIDIFF_VERSION}"
86+
7487
- name: Coverage Report
7588
if: inputs.coverage_report == 'true'
7689
uses: ./.github/actions/go-coverage
7790
with:
7891
coverage_file: ./coverage.out
7992
threshold: ${{ inputs.coverage_threshold }}
93+
94+
- name: Check SDK API compatibility
95+
if: inputs.apidiff_version != ''
96+
shell: bash
97+
run: make api-diff

.github/actions/install-e2e-tools/action.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,16 @@ runs:
4949
chmod +x tools/setup-tools
5050
AUTO_MODE=true ./tools/setup-tools --skip-go --skip-docker
5151
52-
- name: Verify tool installations
52+
- name: Report tool installations
5353
shell: bash
5454
run: |
5555
echo "=== Installed Tools ==="
56-
make tools-check
56+
# This action reports the shared development-tool state, but E2E owns
57+
# its required-tool contract. Do not make future strict_exact tools
58+
# outside that contract a prerequisite for every E2E workflow.
59+
if make tools-check; then
60+
tools_check_rc=0
61+
else
62+
tools_check_rc=$?
63+
printf '%s\n' "::warning title=Non-gating tool check failed::make tools-check exited with status ${tools_check_rc}; E2E continues because its required-tool contract is narrower."
64+
fi

.github/actions/load-versions/action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ outputs:
3131
golangci_lint:
3232
description: 'golangci-lint version'
3333
value: ${{ steps.versions.outputs.golangci_lint }}
34+
apidiff:
35+
description: 'API-diff version pinned in .settings.yaml'
36+
value: ${{ steps.versions.outputs.apidiff }}
3437
addlicense:
3538
description: 'addlicense version'
3639
value: ${{ steps.versions.outputs.addlicense }}
@@ -171,6 +174,7 @@ runs:
171174
172175
# Linting
173176
echo "golangci_lint=$(yq eval '.linting.golangci_lint' .settings.yaml)" >> $GITHUB_OUTPUT
177+
echo "apidiff=$(yq eval '.linting.apidiff' .settings.yaml)" >> $GITHUB_OUTPUT
174178
echo "yamllint=$(yq eval '.linting.yamllint' .settings.yaml)" >> $GITHUB_OUTPUT
175179
echo "addlicense=$(yq eval '.linting.addlicense' .settings.yaml)" >> $GITHUB_OUTPUT
176180
echo "go_licenses=$(yq eval '.linting.go_licenses' .settings.yaml)" >> $GITHUB_OUTPUT
@@ -230,6 +234,7 @@ runs:
230234
echo " ko: ${{ steps.versions.outputs.ko }}"
231235
echo " crane: ${{ steps.versions.outputs.crane }}"
232236
echo " golangci_lint: ${{ steps.versions.outputs.golangci_lint }}"
237+
echo " apidiff: ${{ steps.versions.outputs.apidiff }}"
233238
echo " yamllint: ${{ steps.versions.outputs.yamllint }}"
234239
echo " addlicense: ${{ steps.versions.outputs.addlicense }}"
235240
echo " go_licenses: ${{ steps.versions.outputs.go_licenses }}"

.github/workflows/qualification.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ jobs:
5151
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
5252
with:
5353
ref: ${{ inputs.ref || '' }}
54+
fetch-depth: 0
5455
persist-credentials: false
5556

5657
- name: Load versions
@@ -64,6 +65,7 @@ jobs:
6465
coverage_report: ${{ inputs.coverage_report }}
6566
coverage_threshold: ${{ steps.versions.outputs.coverage_threshold }}
6667
helm_version: ${{ steps.versions.outputs.helm }}
68+
apidiff_version: ${{ steps.versions.outputs.apidiff }}
6769

6870
lint:
6971
name: Lint

.settings.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ build_tools:
2929

3030
# Linting
3131
linting:
32+
# renovate: datasource=go depName=golang.org/x/exp depType=linting
33+
apidiff: 'v0.0.0-20260727155853-b88d891fe743'
3234
# renovate: datasource=github-releases depName=golangci/golangci-lint depType=linting
3335
golangci_lint: 'v2.12.2'
3436
# renovate: datasource=pypi depName=yamllint depType=linting

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,12 @@ scan: ## Scans for vulnerabilities with grype
288288
echo "Running vulnerability scan..."; \
289289
grype dir:. --config .grype.yaml --fail-on high --quiet
290290

291+
.PHONY: api-diff
292+
api-diff: ## Checks pkg/client/v1 compatibility against the latest stable release
293+
@bash tools/api-diff
294+
291295
.PHONY: qualify
292-
qualify: test-coverage lint tuning-check e2e scan license-check ## Qualifies the codebase (test-coverage, lint, tuning-check, e2e, scan)
296+
qualify: test-coverage lint tuning-check e2e scan license-check api-diff ## Qualifies the codebase (test-coverage, lint, tuning-check, e2e, scan, API compatibility)
293297
@echo "Codebase qualification completed"
294298

295299
.PHONY: bom

docs/contributor/maintaining.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,48 @@ The short form:
2727

2828
Bi-weekly cadence; hotfix between cycles when a fix is critical.
2929

30+
### SDK API Compatibility
31+
32+
`make api-diff` compares the exported `pkg/client/v1` surface to the latest
33+
stable release tag. It runs through `make qualify` and the qualification
34+
workflow; additive changes are reported, while removals and incompatible type
35+
changes fail the gate.
36+
37+
Local runs require the repository-pinned `apidiff` and `yq`; install them with
38+
`make tools-setup`. They also require full tag history and a stable release tag
39+
reachable from `HEAD`. The gate checks out that baseline in a temporary
40+
detached worktree, so it leaves the current working tree unchanged but adds
41+
checkout and filesystem I/O cost to `make qualify`.
42+
43+
The gate compares declarations exported by `pkg/client/v1`. For external named
44+
types reached through transparent aliases, `apidiff` matches the target's
45+
package path and type name but does not recursively compare its definition. A
46+
change to such a target can therefore alter the facade contract without failing
47+
the gate. Reviewers must manually assess changes to aliased target types as
48+
changes to the stable facade until
49+
[#2019](https://github.com/NVIDIA/aicr/issues/2019) resolves this gap.
50+
51+
To acknowledge an intentional break, first run `make api-diff`. Add a
52+
baseline-scoped entry to `pkg/client/v1/api-diff-exceptions.yaml` containing the
53+
reported baseline plus non-empty `issue`, `summary`, and `rationale` fields.
54+
Exactly one acknowledgement entry is allowed per baseline. Copy every reported
55+
incompatible line that begins with `- ` into `incompatible_changes`, omitting
56+
the `- ` prefix. The `incompatible_changes` list must exactly and completely
57+
match the command output; omissions and extras both fail the gate.
58+
59+
The acknowledgement authorizes a break only for the active baseline, so keep it
60+
in place through the release that ships that break — it is what keeps the gate
61+
green until the release tag lands. Once the tag advances the baseline, the entry
62+
is obsolete and should be pruned. The gate is deliberately asymmetric about
63+
this. When the diff against the new baseline is clean there is nothing for a
64+
stale entry to authorize, so the gate only warns that the entry is prunable and
65+
still exits successfully; this is what lets the release pipeline and open pull
66+
requests stay green while the cleanup lands. When the diff is *not* clean, a
67+
stale entry is a hard failure: an acknowledgement scoped to an older baseline
68+
must never be accepted for a break against the current one. Prune the entry in a
69+
follow-up change after the release; Git history retains the release-notes record
70+
of the breaking change.
71+
3072
### Common Release Breakages
3173

3274
**`goreleaser` fails with auth conflict.** `goreleaser` panics if both

docs/contributor/tests.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,8 @@ half of the pipeline and skips deploy-side assertions.
483483
- `e2e` — the end-to-end pipeline runner.
484484
- `scan` — Grype vulnerability scan.
485485
- `license-check` — license header / dependency-license sweep.
486+
- `api-diff` — exported `pkg/client/v1` compatibility against the latest stable
487+
release.
486488

487489
CI runs the equivalent. If `make qualify` passes locally on the
488490
current branch, push CI will pass.

docs/integrator/public-api.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ The `github.com/NVIDIA/aicr/pkg/client/v1` package is Public (stable). Types
5656
reachable from this surface are either facade-owned structs or transparent
5757
aliases — the table below documents which.
5858

59+
Transparent aliases extend that stable contract to their target types. The
60+
automated API-diff gate matches an external named target by package path and
61+
type name, but does not recursively compare the target's definition. Until
62+
[#2019](https://github.com/NVIDIA/aicr/issues/2019) resolves this limitation,
63+
changes to those definitions require manual compatibility review because they
64+
can affect facade consumers without failing the gate.
65+
5966
| Facade symbol | Translates to/from | Notes |
6067
|---|---|---|
6168
| `aicr.Snapshot` | `pkg/snapshotter.Snapshot` | **Facade-owned struct**. Public fields are identifying metadata; full measurement payload is preserved in an unexported field for round-trip through `ValidateState`. Use `aicr.WrapSnapshot` to lift a `*snapshotter.Snapshot` loaded externally. |

0 commit comments

Comments
 (0)