Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .agents/skills/aicr-release-notes/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,15 @@ order:
3. **`**Theme Name**` blocks** — each starts with bolded title, em dash
(` — `, with spaces), then 1–3 sentences OR a bulleted sub-list.
Use sub-lists when enumerating 3+ concrete items (e.g. recipes added).
4. **Closing credits line** — `***Thanks to*** @user1, @user2, …, and
4. **`### Deprecations`** — **required whenever the release deprecates or
removes anything on the four frozen surfaces** (CLI, REST, Go SDK, bundle
and artifact schemas); omit the heading entirely when it does not. One
bullet per item: what is deprecated, the replacement, and the release that
removes it. This is a release-blocking section, not a courtesy — see the
[deprecation policy](https://github.com/NVIDIA/aicr/blob/main/RELEASING.md#deprecation-policy).
Every bullet here must also have an entry in `docs/user/deprecations.md`;
if it does not, the deprecation is incomplete and the release is not ready.
5. **Closing credits line** — `***Thanks to*** @user1, @user2, …, and
@mchmarny.` Alphabetical (case-insensitive) by handle, with
`@mchmarny` moved to the final position preceded by `and `.

Expand Down
80 changes: 80 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,86 @@ Releases follow a **bi-weekly cadence**. A new release is cut every two weeks.
| Pre-release | Before a regular release, as needed | `rc` | Any maintainer can create for testing |
| Major | Planned | `major` | Requires team agreement and advance communication |

## Deprecation Policy

AICR freezes four public surfaces at v1
([ROADMAP §1](ROADMAP.md#1-defensible-api-stability)):
the `aicr` CLI, the REST API, the Go SDK (`pkg/client/v1`), and the bundle
layout plus artifact schemas. This section defines what counts as a breaking
change on each, the notice a removal owes, and how a deprecation reaches the
people affected by it. A change that is breaking under this table and ships
without the notice below is a release blocker, not a release note.

### What counts as breaking, per surface

| Surface | Breaking | Additive |
|---|---|---|
| **CLI** — flags, subcommands, exit codes, stdout shape | Removing or renaming a flag or subcommand; changing a default such that identical input yields different output; narrowing an accepted value set; changing what an exit code means; removing a field from `--output json`/`yaml` | New flag whose default preserves current behavior; new subcommand; new accepted enum value; new field in structured output |
| **REST** — `api/aicr/v1/server.yaml` | Removing a path or method; removing or renaming a response field; adding a required request field; narrowing a type; removing a value from a request enum | New optional request field; new response field; new path or method; new value in a response enum |
| **Go SDK** — `pkg/client/v1` | Removing or renaming an exported identifier; changing a signature; narrowing a parameter type; changing documented semantics without changing the name | New exported function, method, or type; new functional option; new field on a struct the caller does not construct positionally |
| **Bundle + schemas** — layout and artifact kinds | Removing or renaming a bundle path; removing a schema field; tightening a type; adding a required field; retiring an `apiVersion` | New optional field; new file in the bundle; new artifact kind |

Adding a value to a *response* enum is additive for the server and breaking for
a client that switches exhaustively on it, so it is announced but does not owe a
window. Adding a value to a *request* enum is always additive; removing one is
always breaking.

### Notice owed before removal

- **Before `v1.0.0`:** a minimum of **two minor releases** between the
deprecation shipping with a working warning and the removal. At the current
cadence that is roughly one month.
- **After `v1.0.0`:** a breaking removal on any of the four frozen surfaces
requires the next `vMAJOR`. The deprecation may be announced at any time; the
removal waits for the major. This is what the freeze buys and it is not
waivable by a release manager.

Artifact `apiVersion` retirement is the one surface with a maturity-scoped
window rather than a flat one, because an alpha version never promised
stability in the first place. Its rules are below and take precedence for that
surface.

### How a deprecation is announced

Every deprecation appears in all three places. One is not a substitute for
another: release notes are read once, the durable page is read later by someone
debugging, and the runtime warning reaches the user who never read either.

1. A `## Deprecations` section in the release notes for the release that

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🟡 Minor — RELEASING.md says release notes carry a ## Deprecations (h2) section, but the aicr-release-notes skill requires ### Deprecations (h3)

This item says "A ## Deprecations section in the release notes," but the authoritative generator (.agents/skills/aicr-release-notes/SKILL.md:108) adds ### Deprecations as an h3 sibling of ### Highlights. Someone following RELEASING.md literally would emit an h2 that breaks the heading hierarchy the skill enforces.

Fix: Change this to ### Deprecations to match the skill's h3 level.

introduces it, naming the replacement and the planned removal release.
2. An entry on the durable page at
[`docs/user/deprecations.md`](docs/user/deprecations.md), which carries every
active deprecation and its removal release until the removal ships.
3. A runtime warning on the affected surface, using that surface's mechanism:

| Surface | Mechanism |
|---|---|
| CLI | Warning on stderr naming the replacement and the removal release. Honors `NO_COLOR` and the existing logger conventions |
| REST | A `Deprecation` response header ([RFC 9745](https://www.rfc-editor.org/rfc/rfc9745.html)) carrying the deprecation date, a `Sunset` header ([RFC 8594](https://www.rfc-editor.org/rfc/rfc8594.html)) carrying the removal date, a `Link` with `rel="deprecation"`, and `deprecated: true` on the operation in `api/aicr/v1/server.yaml` |
| Go SDK | A `// Deprecated:` godoc marker, which `staticcheck` surfaces to consumers automatically |
| Bundle + schemas | The loader accepts the deprecated shape and warns, naming the file and the release that stops reading it |

### Exercising the channel before `v1.0.0`

ROADMAP [§1](ROADMAP.md#1-defensible-api-stability) requires this file to define
breaking changes and the deprecation policy for every surface; it does not
require a rehearsal. Manufacturing a deprecation to prove the channel works
would prove only that we can manufacture one.

Two real ones are already scheduled, and they are the exercise:

- The `/v1/*` REST path family retirement ([#2112](https://github.com/NVIDIA/aicr/issues/2112))
is the one that drives the RFC 9745 `Deprecation` header, the RFC 8594
`Sunset` header, and the OpenAPI `deprecated` flag — the arm integrators
actually consume.
- The ADR-022 alpha-to-target artifact migration runs warn-then-remove across
v0.22 and v0.23. It is a genuine warn-then-remove cycle, but a weaker
demonstration on its own: alpha owes no window under the table above, so it
does not show the channel honoring an obligation it actually had.

If either turns up a gap in the mechanism, fix it then. That is cheaper and
more honest than gating the release on a dry run.

## Artifact Compatibility and Deprecation

Artifact `apiVersion` maturity is independent of the AICR release version and
Expand Down
33 changes: 33 additions & 0 deletions docs/contributor/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,39 @@ Rules:
(`config_helpers_test.go`, `bundle_resolve_helpers_test.go`) are
the template.

## The CLI Surface Baseline

The CLI is one of the four surfaces frozen at v1
([ROADMAP §1](https://github.com/NVIDIA/aicr/blob/main/ROADMAP.md#1-defensible-api-stability)).
`pkg/cli/testdata/cli-surface.golden`
is its committed inventory — every command, flag, alias, type, default,
`required`/`hidden` state, and environment variable — and `TestCLISurface`
(`pkg/cli/surface_test.go`) fails when the live tree stops matching it. It runs
under `make test`, so it is already inside the merge gate; no separate workflow
is involved.

**If you added a command or flag,** the addition is compatible. Regenerate and
commit the result in the same PR:

```bash
go test ./pkg/cli/ -run TestCLISurface -update
```

Scope the `-update` flag to `./pkg/cli/` — it is registered only by this test,
so `go test ./... -update` fails in every other package.

**If you removed or renamed a command, flag, or alias, or changed a default,**
the test reports it as `BREAKING` rather than telling you to regenerate. That is
a breaking change to a frozen surface and it owes the notice period in
[`RELEASING.md`](https://github.com/NVIDIA/aicr/blob/main/RELEASING.md#deprecation-policy): ship the deprecation
with a warning first, remove it only after the window, and add an entry to
[`docs/user/deprecations.md`](../user/deprecations.md). Regenerate the golden
only once the removal is actually due.

Usage strings are deliberately not pinned. They are prose, they change for good
reasons, and including them would make the gate fail on every wording fix — the
fastest way to train everyone to run `-update` without reading the diff.

## Anti-Patterns

| Don't | Do |
Expand Down
29 changes: 26 additions & 3 deletions docs/design/022-artifact-maturity-and-deprecation.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ already accepts `aicr.run/v1beta1`; a kind introduced now can start at its
target without violating §7's rule that a new kind is never stamped with a
version the tree does not accept, and without a shipped alpha version to retire.

Revised 2026-08-28 for [#2421](https://github.com/NVIDIA/aicr/issues/2421): §3
states which clause governs a catalog kind that arrives through the direct
recipe-input path, and scopes the surviving empty-`apiVersion` tolerance to
`RecipeResult`.

Amends [ADR-011](011-artifact-apiversion-policy.md): §1 keeps `pkg/header` as the
single source of version strings but replaces its single-version alias rule;
§3 becomes kind/schema-scoped, covers AICR catalog inputs, and retires its
Expand All @@ -27,7 +32,7 @@ kind-scoped version evolution as an amendment to ADR-011.
## Problem

Every artifact AICR generates today carries an alpha `apiVersion`. ROADMAP
2](../../ROADMAP.md#2-stability) promises a frozen, diff-gated surface at v1,
1](../../ROADMAP.md#1-defensible-api-stability) promises a frozen, diff-gated surface at v1,
and the Kubernetes convention that `v1alpha2` invokes — may be dropped or changed
without notice — is the opposite of that promise. Two alpha schema tracks coexist:
`aicr.run/v1alpha2` for general kinds and default/catalog forms, and
Expand Down Expand Up @@ -68,7 +73,7 @@ Four questions have no recorded answer:
### 1. Project v1 and artifact `v1` are separate axes

AICR reaching v1.0.0 does not require every artifact kind to reach
`aicr.run/v1`. ROADMAP §2 asks for a committed baseline, a CI diff-gate, and a
`aicr.run/v1`. ROADMAP §1 asks for a committed baseline, a CI diff-gate, and a
deprecation channel. A gate over a `v1beta1` schema is a real gate: it catches
*unintended* breakage, which is what the freeze promises. The maturity string
governs *intended* breakage. It is selected by wire kind and current schema
Expand Down Expand Up @@ -190,6 +195,24 @@ predating the field; `AICRConfig` already rejects it. After N+1 emits only
target versions, an unversioned artifact would otherwise pass those gates
unchallenged — the fail-open shape §8 exists to close.

**A catalog kind is governed by §8 on every path it can arrive by.** The
tolerance above is scoped by wire kind, not by entry point. A `RecipeMetadata`
reaching AICR as a direct recipe input (`aicr recipe -r overlay.yaml`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same invalid command in the clause defining which paths the gate governs. The direct-input paths are aicr bundle -r and aicr validate -r.

`aicr bundle -r overlay.yaml`) is the same catalog document it would be inside a
`--data` tree, so it is held to the same fail-closed authoring gate the catalog
scanner applies, including the rejection of an empty value. §3 step 1's
"existing empty-value tolerances remain where they already exist" does not
extend a tolerance to a document the catalog path already rejects; where the two
paths disagreed, the stricter one governs.

This resolves [#2421](https://github.com/NVIDIA/aicr/issues/2421), where
`pkg/recipe/loader.go` short-circuited on an empty value before it inspected the
kind, so a headerless overlay was rejected from a `--data` tree and silently
hydrated when passed with `-r`. Closing it in Release N rather than deferring to
N+1 keeps the two paths from disagreeing across the release where the emitter
switch rewrites every committed header. The empty-value tolerance survives for
`RecipeResult` inputs only, and retires with the rest at N+2.

### 4. The deprecation window is conditional on the level being retired

This **replaces ADR-011 §4**, whose dual-accept rule was stated unconditionally.
Expand Down Expand Up @@ -351,5 +374,5 @@ intent; silent downgrade is not.
- [ADR-011](011-artifact-apiversion-policy.md) — artifact `apiVersion` policy and compatibility gate
- [ADR-013](013-aicr-run-domain-migration.md) — `aicr.run` domain migration, the precedent for a pre-v1 hard break
- [ADR-015](015-recipe-configuration-profiles.md) — recipe configuration profiles, which introduced kind-scoped evolution
- [ROADMAP §2 Stability](../../ROADMAP.md#2-stability)
- [ROADMAP §1 Defensible API stability](../../ROADMAP.md#1-defensible-api-stability)
- [Kubernetes deprecation policy](https://kubernetes.io/docs/reference/using-api/deprecation-policy/)
2 changes: 2 additions & 0 deletions docs/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ navigation:
path: user/artifact-verification.md
- page: API Reference
path: user/api-reference.md
- page: Deprecations
path: user/deprecations.md
- page: Agent Deployment
path: user/agent-deployment.md
- page: Air-Gapped Mirroring
Expand Down
10 changes: 10 additions & 0 deletions docs/integrator/data-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ embedded one and silently carried forward. Unrelated YAML in the tree keeps its
existing skip behavior, and `ValidatorCatalog` sits on a separate API domain
outside this contract.

This gate follows the document, not the entry point. Passing a single overlay
directly — `aicr recipe -r overlay.yaml`, `aicr bundle -r overlay.yaml` —

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same invalid command. aicr recipe -r overlay.yaml should be aicr validate -r overlay.yaml, alongside the aicr bundle -r already cited here.

applies the same check as a `--data` catalog scan, so a `RecipeMetadata`
with a missing or empty `apiVersion` is rejected on both paths. Through v0.20
the direct path accepted it and hydrated silently
([#2421](https://github.com/NVIDIA/aicr/issues/2421)); if you author overlays
outside a catalog tree, confirm each one carries a header. The empty-value
tolerance that remains is for hydrated `RecipeResult` inputs only, and it
retires in v0.23.

## Adding a criteria value

Criteria value validation (`service`, `accelerator`, `intent`, `os`,
Expand Down
27 changes: 27 additions & 0 deletions docs/integrator/go-library.md
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,33 @@ Versioning][semver]:
- **Minor** bumps may add new exported types, fields, or methods.
- **Patch** bumps contain compatible bug fixes.

### How you learn something is going away

Nothing in `pkg/client/v1` is removed without first being marked deprecated for
the notice period in
[`RELEASING.md`](https://github.com/NVIDIA/aicr/blob/main/RELEASING.md#deprecation-policy)
— two minor releases before v1.0, and after v1.0 the next major.

The marker is a standard Go `// Deprecated:` godoc paragraph on the identifier:

```go
// ResolveRecipe returns a resolved recipe for the given criteria.
//
// Deprecated: use [Client.Resolve] instead. ResolveRecipe is removed in v0.25.
func (c *Client) ResolveRecipe(...) { ... }
```

This is deliberately not a runtime warning. `staticcheck` reports `SA1019` for
every use of a deprecated identifier, so the notice arrives in your build — at
the point you can act on it — rather than in a log line from a production run.
`go doc`, `gopls`, and every major Go IDE surface the same paragraph. If you run
`staticcheck` (or `golangci-lint` with the `staticcheck` linter enabled) in CI,
you get the deprecation channel for free with no AICR-specific tooling.

Each marker names the replacement and the removal release. The complete list of
active deprecations across all surfaces is in
[Deprecations](../user/deprecations.md).

## See also

- [Public API surface](./public-api.md) — stability matrix per package
Expand Down
14 changes: 9 additions & 5 deletions docs/user/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,11 +599,15 @@ CLI file loader for the same values — `aicr bundle -r` accepts a
The shared artifact gate rejects any `apiVersion` outside
`aicr.run/v1alpha2`, `aicr.run/v1`, `aicr.run/v1alpha3`, and
`aicr.run/v1beta2` with a 400, on this endpoint as well as on the CLI file-load
path. An absent or empty `apiVersion` is still admitted as the legacy shape
through v0.22, and v0.23 stops admitting it along with the alpha values. The
reader and emitter clocks are separate: v0.21 and v0.22 both read the alpha
values, the target values, and the empty header, while generated recipes keep
their alpha headers until v0.22 switches the emitters. See
path. An absent or empty `apiVersion` is still admitted as the legacy shape on
`RecipeResult` inputs through v0.22, and v0.23 stops admitting it along with the
alpha values. The tolerance is scoped to `RecipeResult`, which predates the
field: a `RecipeMetadata` overlay is a catalog document however it arrives, so
`aicr recipe -r` and `aicr bundle -r` reject a headerless one exactly as a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same invalid command: aicr recipe -r does not exist. Should read aicr bundle -r and aicr validate -r.

`--data` catalog scan does. The reader and emitter clocks are separate: v0.21
and v0.22 both read the alpha values, the target values, and the empty header,
while generated recipes keep their alpha headers until v0.22 switches the
emitters. See
[Catalog and binary compatibility](../integrator/data-extension.md#catalog-and-binary-compatibility)
for the release-by-release table.

Expand Down
Loading
Loading