Skip to content

Commit 2f92468

Browse files
committed
refactor: remove "next" key from compat manifest, add DATABRICKS_FORCE_EMBEDDED_COMPAT
The manifest is now purely range-based: each versioned entry defines a range floor that applies to that CLI version and all above it. The "next" key was redundant since we always know the CLI version when updating the manifest. Dev builds now resolve to the highest versioned entry. Also adds DATABRICKS_FORCE_EMBEDDED_COMPAT=true env var to skip remote fetch and use only the embedded manifest, useful for local development. Co-authored-by: Isaac
1 parent 1491783 commit 2f92468

5 files changed

Lines changed: 146 additions & 187 deletions

File tree

.agent/skills/bump-cli-compat/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Read `internal/build/cli-compat.json`. Note the current versions and the list of
6868

6969
### Step 4: Update the manifest
7070

71-
Update **all entries** (both `next` and all versioned CLI entries) to the new appkit and skills versions. This is the "no template changes" scenario — a simple search & replace.
71+
Update the **highest versioned entry** to the new appkit and skills versions. Do NOT add new versioned keys — this is the default "no breaking changes" flow. The manifest is range-based: updating the highest entry automatically covers all CLI versions in that range.
7272

7373
Write the updated `internal/build/cli-compat.json`.
7474

internal/build/README.md

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,27 @@
66

77
```json
88
{
9-
"next": { "appkit": "0.24.0", "skills": "0.1.5" },
10-
"0.300.0": { "appkit": "0.24.0", "skills": "0.1.5" }
9+
"0.296.0": { "appkit": "0.27.0", "skills": "0.1.5" },
10+
"0.290.0": { "appkit": "0.24.0", "skills": "0.1.4" },
11+
"0.280.0": { "appkit": "0.20.0", "skills": "0.1.0" }
1112
}
1213
```
1314

14-
- Each key is a CLI version (`X.Y.Z`) or `"next"`.
15-
- Each value specifies the compatible `appkit` and `skills` versions.
16-
- `"next"` is used for dev builds (`0.0.0-dev*`). For production CLI versions newer than all listed entries, the highest versioned entry is used.
15+
Each key is a CLI version in semver format. Each entry defines a **range floor**: it applies to that CLI version and all versions above it, up to (but not including) the next entry. The manifest should be **sparse** — not every CLI version needs its own entry. Only add a new entry when a compatibility boundary changes.
16+
17+
For example, with the manifest above:
18+
- CLI `0.285.0` → uses `0.280.0` entry (appkit `0.20.0`)
19+
- CLI `0.293.0` → uses `0.290.0` entry (appkit `0.24.0`)
20+
- CLI `0.300.0` → uses `0.296.0` entry (appkit `0.27.0`, highest versioned)
21+
- CLI `0.0.0-dev+abc` → uses `0.296.0` entry (dev builds use the highest versioned entry)
1722

1823
## How the CLI resolves versions
1924

20-
1. **Exact match** on CLI version → use that entry.
21-
2. **No exact match**, between two entries → use the nearest lower version's entry.
22-
3. **Newer than all entries** → use the highest versioned entry.
23-
4. **Older than all entries** → use the lowest (oldest) entry.
24-
5. **Dev builds** (`0.0.0-dev*`) → use `"next"`.
25+
1. **Dev builds** (`0.0.0-dev*`) → use the highest versioned entry.
26+
2. **Exact match** on CLI version → use that entry.
27+
3. **No exact match**, between two entries → use the nearest lower version's entry.
28+
4. **Newer than all entries** → use the highest versioned entry.
29+
5. **Older than all entries** → use the lowest (oldest) entry.
2530

2631
## Manifest sources (fallback chain)
2732

@@ -32,15 +37,16 @@ At runtime, the CLI resolves the manifest from four sources:
3237
3. **Stale local cache** — if remote fetch fails but a previously cached file exists (even if expired), it is used as-is.
3338
4. **Embedded manifest** — compiled into the binary via `go:embed`. Used as last resort when both remote and local cache fail.
3439

40+
Set `DATABRICKS_FORCE_EMBEDDED_COMPAT=true` to skip all tiers and use only the embedded manifest. This is useful for local development when testing with a locally compiled binary that has a modified `cli-compat.json`.
41+
3542
## When to update
3643

37-
After each AppKit or Agent Skills release:
44+
The goal is to **keep the manifest sparse** — only add entries at compatibility boundaries. After each AppKit or Agent Skills release:
3845

3946
1. **Run evals** on the new AppKit version. If there is no regression, proceed.
4047
2. **Open a PR** to update `cli-compat.json`. The change depends on the type of release:
41-
- **No template changes** (just an AppKit/skills version bump): search & replace all version occurrences in the manifest and update `next`.
42-
- **Template changes that don't require new CLI features**: test the last 3 CLI versions with the new template and update matching entries.
43-
- **Template changes that require new CLI features**: add a new entry for the minimum CLI version that supports them; older entries keep pointing to the previous template version.
48+
- **No breaking changes** (the new AppKit/skills version works with all existing CLI versions): update the existing highest versioned entry's appkit/skills values in-place. Do NOT add a new versioned key. All CLI versions in that range automatically pick up the new versions.
49+
- **Breaking changes** (the new AppKit templates require specific `apps init` features, or the new skills version requires CLI commands that older CLIs lack): add a new entry keyed to the **minimum CLI version** that supports the new features. Older entries keep their previous appkit/skills values so older CLI binaries stay compatible.
4450

4551
This process is manual for now but can be automated as part of the release workflow in the future. Use the `/bump-cli-compat` Claude Code skill to automate the update and PR creation.
4652

@@ -52,7 +58,7 @@ The manifest is validated by Go tests in `libs/clicompat/`:
5258
go test ./libs/clicompat/... -run TestEmbeddedManifest -v
5359
```
5460

55-
This checks: valid JSON, `"next"` key present, at least one versioned entry, valid semver keys, valid semver entry values, `next` versions >= all entries, and ascending key order.
61+
This checks: valid JSON, at least one entry, valid semver keys, valid semver entry values, and ascending key order.
5662

5763
## Pruning policy
5864

internal/build/cli-compat.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
{
2-
"next": { "appkit": "0.24.0", "skills": "0.1.5" },
32
"0.300.0": { "appkit": "0.24.0", "skills": "0.1.5" }
43
}

libs/clicompat/clicompat.go

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,15 @@ const (
4141
// fetchTimeout is the HTTP timeout for fetching the manifest at runtime.
4242
fetchTimeout = 3 * time.Second
4343

44-
// nextKey is the special manifest key for CLI versions newer than any entry.
45-
nextKey = "next"
46-
4744
// cacheTTL is how long a locally cached manifest is considered fresh.
4845
cacheTTL = 1 * time.Hour
4946

5047
// localManifestFile is the filename for the locally cached manifest.
5148
localManifestFile = "compat-manifest.json"
5249

53-
// devVersionPrefix identifies dev builds that always use the "next" entry.
50+
// devVersionPrefix identifies dev builds whose semver (0.0.0) is lower than
51+
// all real CLI versions. These are treated as bleeding-edge and resolve to
52+
// the highest versioned entry.
5453
devVersionPrefix = "0.0.0-dev"
5554

5655
maxFetchAttempts = 3
@@ -88,9 +87,18 @@ var httpClient = &http.Client{Timeout: fetchTimeout}
8887
// 3. Stale local file (if remote fails but a previously cached file exists)
8988
// 4. Embedded manifest compiled into the binary
9089
//
90+
// Set DATABRICKS_FORCE_EMBEDDED_COMPAT=true to skip all tiers and use only
91+
// the embedded manifest. Useful for local development when testing with a
92+
// locally compiled binary.
93+
//
9194
// Set DATABRICKS_CACHE_ENABLED=false to bypass the local cache (tiers 1 and 3a),
9295
// which is useful to recover from a bad cached manifest.
9396
func FetchManifest(ctx context.Context) (Manifest, error) {
97+
if force, ok := env.GetBool(ctx, "DATABRICKS_FORCE_EMBEDDED_COMPAT"); ok && force {
98+
log.Debugf(ctx, "Using embedded manifest (DATABRICKS_FORCE_EMBEDDED_COMPAT=true)")
99+
return parseEmbeddedManifest()
100+
}
101+
94102
cacheEnabled := true
95103
if enabled, ok := env.GetBool(ctx, "DATABRICKS_CACHE_ENABLED"); ok {
96104
cacheEnabled = enabled
@@ -197,8 +205,11 @@ func IsNotFoundError(err error) bool {
197205

198206
// Resolve returns the manifest entry for the given CLI version.
199207
//
208+
// Each versioned entry defines a range floor: it applies to that CLI version
209+
// and all versions above it, up to (but not including) the next entry.
210+
//
200211
// Resolution order:
201-
// 1. Dev builds (version starts with "0.0.0-dev") use the "next" entry.
212+
// 1. Dev builds (version starts with "0.0.0-dev") use the highest versioned entry.
202213
// 2. Exact match on CLI version.
203214
// 3. Nearest lower version (semver-sorted). This also handles CLI versions
204215
// newer than all entries, returning the highest known entry.
@@ -208,34 +219,24 @@ func Resolve(m Manifest, cliVersion string) (Entry, error) {
208219
return Entry{}, errors.New("empty compatibility manifest")
209220
}
210221

211-
next, ok := m[nextKey]
212-
if !ok {
213-
return Entry{}, fmt.Errorf("compatibility manifest missing %q key", nextKey)
222+
// Collect and sort versioned keys descending.
223+
versions := sortedVersions(m)
224+
if len(versions) == 0 {
225+
return Entry{}, errors.New("compatibility manifest has no versioned entries")
214226
}
215227

216-
// Dev builds always use "next".
228+
// Dev builds (0.0.0-dev*) have semver lower than all real CLI versions,
229+
// so they would incorrectly resolve to the lowest entry. Use the highest
230+
// versioned entry instead, since dev builds represent the bleeding edge.
217231
if strings.HasPrefix(cliVersion, devVersionPrefix) {
218-
return next, nil
232+
return m[versions[0]], nil
219233
}
220234

221235
// Exact match.
222236
if entry, ok := m[cliVersion]; ok {
223237
return entry, nil
224238
}
225239

226-
// Collect and sort versioned keys (exclude "next").
227-
var versions []string
228-
for k := range m {
229-
if k != nextKey {
230-
versions = append(versions, k)
231-
}
232-
}
233-
234-
// Sort descending by semver. The semver package requires a "v" prefix.
235-
slices.SortFunc(versions, func(a, b string) int {
236-
return semver.Compare("v"+b, "v"+a)
237-
})
238-
239240
// Find the nearest lower version.
240241
vCLI := "v" + cliVersion
241242
for _, v := range versions {
@@ -244,14 +245,22 @@ func Resolve(m Manifest, cliVersion string) (Entry, error) {
244245
}
245246
}
246247

247-
// CLI is older than all entries — use the lowest (oldest) entry as closest match.
248-
// If there are no versioned entries (only "next"), fall back to "next".
249-
if len(versions) == 0 {
250-
return next, nil
251-
}
248+
// CLI is older than all entries — use the lowest (oldest) entry.
252249
return m[versions[len(versions)-1]], nil
253250
}
254251

252+
// sortedVersions returns manifest keys sorted descending by semver.
253+
func sortedVersions(m Manifest) []string {
254+
versions := make([]string, 0, len(m))
255+
for k := range m {
256+
versions = append(versions, k)
257+
}
258+
slices.SortFunc(versions, func(a, b string) int {
259+
return semver.Compare("v"+b, "v"+a)
260+
})
261+
return versions
262+
}
263+
255264
// resolveEntry fetches the manifest, resolves for the given CLI version.
256265
func resolveEntry(ctx context.Context, cliVersion string) (Entry, error) {
257266
m, err := FetchManifest(ctx)
@@ -418,11 +427,8 @@ func parseManifest(data []byte) (Manifest, error) {
418427
if len(m) == 0 {
419428
return nil, errors.New("empty compatibility manifest")
420429
}
421-
if _, ok := m[nextKey]; !ok {
422-
return nil, fmt.Errorf("compatibility manifest missing %q key", nextKey)
423-
}
424430
for k := range m {
425-
if k != nextKey && !semver.IsValid("v"+k) {
431+
if !semver.IsValid("v" + k) {
426432
return nil, fmt.Errorf("invalid semver key %q in compatibility manifest", k)
427433
}
428434
}

0 commit comments

Comments
 (0)