Skip to content

feat(config): warn about avocado.yaml keys the cli ignores - #286

Open
lee-reinhardt wants to merge 1 commit into
mainfrom
config-unknown-key-warnings
Open

lee-reinhardt wants to merge 1 commit into
mainfrom
config-unknown-key-warnings

Conversation

@lee-reinhardt

Copy link
Copy Markdown
Member

Problem

A misspelled key in avocado.yaml does nothing and says nothing. A user evaluating Avocado hit this with extentions: in a runtime (the runtime came out with zero extensions) and permissions: { user: ... }, and asked for warnings and a usable schema.

The config structs use serde's default of dropping unknown fields, and large parts of the file are never typed at all: extensions, overlays, users and groups, and per-target override blocks are read as raw YAML, so a key nobody looks up is simply skipped. In rootfs, initramfs, kernel and permissions it's worse: a section whose keys are all misspelled has no recognised field, so it parses as a named entry, silently changing what the file means.

The published JSON schema didn't help. It lived in the docs repo, maintained by hand, and had drifted: no rootfs, initramfs, permissions, repos or connect, most extension keys missing, an open top level, and a $id that returns 404.

Change

The schema moves here (schemas/avocado-config.json), rebuilt from a key-by-key inventory of what the code reads, typed and raw, at the level it reads it. Every description was checked against the code. Its $id is the docs URL; the docs site will publish this copy (separate PR).

The CLI warns about every key it ignores (src/utils/config_lint.rs), walking the user's own file against the embedded schema:

[WARNING] avocado.yaml: unknown key 'runtimes.dev.extentions' is ignored; did you mean 'extensions'?
[WARNING] avocado.yaml: 'permissions.user' sets no permissions fields, so it is read as a named permissions entry; did you mean the field 'users'?
[WARNING] avocado.yaml: 'ext' is an old name for 'extensions' and is no longer read; rename it to 'extensions'
  • It warns, never fails, so a file an older CLI accepted still builds.
  • It runs from both loaders (Config::load, load_composed_with_board) once per file per process, keyed on the canonical path, since commands load the same file many times.
  • It prints to stderr and stays silent under --output json, where config show --output json carries the messages in a warnings array instead. The array is present only when there are warnings, so clean output is byte-identical.
  • Renamed keys (ext, runtime, sdk.dependencies, the sysext/confext booleans) and keys that parse but do nothing (sdk.host_uid, a user's disabled, a group's password) get a specific message from an x-avocado-warning in the schema.
  • Keys containing {{ }} are treated as names. A top-level key the file reads back through {{ config.<key> }} is allowed. A bare target-name override is accepted where the CLI resolves one, recognised by shape (it sets the block's own fields), since the target may only be known from --target or after interpolation.
  • For the four one-entry-or-named-map sections, it mirrors the CLI's shape inference, including that source counts as a path source only with type: path.

The avocado init template starts with a yaml-language-server schema comment, so VS Code with the YAML extension gets autocomplete, hover docs and validation.

strsim becomes a direct dependency for the suggestions; it was already in the lock file through clap.

Keeping the schema current

schema_describes_every_typed_config_field fails when a typed config struct accepts a field (or alias) the schema doesn't describe, naming the missing pointer. Keys read raw are not covered: a PR that adds a raw read has to add the key to the schema by hand. Typing the raw sections, starting with extensions, would bring them under the same test.

Tests

  • 20 unit tests in config_lint: clean configs, suggestions, renamed and no-effect keys, the named-entry trap, tagged source branches, templated keys and tags, bare-target overrides, interpolation variables, sequence paths, the drift guard (confirmed by deleting a field and an alias from the schema), and a check that the repo's own configs and fixtures produce no warnings.
  • All 81 avocado.yaml files in avocado-linux/references and avocado-os produce zero warnings, run through avocado config show.
  • cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings and all integration tests pass. On macOS, 5 existing lib tests in stamps.rs and ext/build.rs fail because they shell out to GNU sed/sha256 tools; they don't touch config loading.

Not in this PR

  • Publishing the schema on the docs site, and the configuration guide's gaps: peridio/docs PRs.
  • Schema comments in the references repo's configs.
  • Linting files other than the user's main config (extension configs, path fragments).
  • The existing missing-env-var warning in interpolation prints to stdout even under --output json, so a config that references an unset env var already produces invalid JSON from config show.

Copilot AI lite review requested due to automatic review settings September 25, 2026 02:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Unresolved moderate schema and linter coverage gaps can allow ignored or valid configuration keys to be misreported.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 Medium severity

Open (2)
What changed in this PR

Adds schema-backed warnings for ignored avocado.yaml keys, with suggestions, JSON output integration, and editor tooling support.

Changes:

  • Adds recursive configuration linting and deprecation warnings.
  • Embeds the configuration schema and updates the default template.
  • Adds warning propagation to config show --output json.
  • Adds strsim as a direct dependency.
File Summary
src/​utils/​mod.rs Registers the config linter module.
src/​utils/​config.rs Runs linting during configuration loads.
src/​utils/​config_lint.rs Implements schema-based analysis and tests.
src/​commands/​config_show.rs Includes warnings in JSON output.
schemas/​avocado-config.json Defines configuration fields and warning metadata.
configs/​default.yaml Adds YAML language-server schema metadata.
Cargo.toml Adds strsim.
Cargo.lock Records the dependency update.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread schemas/avocado-config.json Outdated
Comment thread schemas/avocado-config.json
The config structs drop fields they don't declare, and much of the file
(extensions, overlays, users and groups, per-target overrides) is only
ever read as raw YAML, so a misspelled key did nothing and said nothing.
In rootfs, initramfs, kernel and permissions a typo was worse: a section
whose keys were all misspelled parsed as a named entry, quietly changing
what the file meant.

The config schema moves here from the docs repo, rebuilt from what the
code actually reads, and the cli walks the user's own file against it.
Every key it ignores gets one warning per run on stderr, with a "did you
mean" suggestion when a known key is close. Renamed keys (ext, runtime,
sdk.dependencies, sysext/confext booleans) and keys that parse but do
nothing say so. It warns and never fails, so a file an older cli
accepted still builds.

Keys with {{ }} templates are treated as names, a top-level key the file
reads back through {{ config.<key> }} is allowed, and a bare target-name
override is recognised by its shape, since its target may only be known
from the command line or after interpolation.

`config show --output json` carries the warnings in a `warnings` array,
present only when there are any, so clean output is unchanged. The init
template points editors at the published schema.

A test fails when a typed config struct accepts a field the schema
doesn't describe. Keys read raw are not covered by it.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Five unresolved moderate issues affect schema coverage and false-negative or false-positive linting behavior.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
Resolved since last review (2)

Comment thread src/utils/config_lint.rs

@jetm jetm left a comment

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.

Nothing blocking. This is careful work: the shape-inference in pick_branch correctly mirrors how the CLI itself disambiguates anyOf branches (tag match, then singleton-by-claimed-field, then named-entry), the sets_no_field/sets_a_field heuristics for named-entry-vs-misspelled-singleton are well-targeted, and schema_describes_every_typed_config_field's trick of extracting serde's own field names through a custom Deserializer is a genuinely good way to keep the schema and the real structs from drifting apart. I traced the JSON-mode double-report question (loading prints to stderr, but --output json also rebuilds the warnings for the payload) and it's handled correctly: print_stderr_notice checks json_requested_on_command_line() and stays quiet in that mode, so there's no double emission.

Two things worth knowing, neither blocking. resolve()'s $ref follower has no cycle guard - a future recursive schema definition would hang it in an infinite loop rather than error, though I checked and the current avocado-config.json has no cycles today. And a handful of other config-reading helpers (get_runtime_names, get_merged_section_with_board) read the file directly rather than going through Config::load/load_composed_with_board, so they don't call warn_ignored_keys_once themselves - I checked runtime clean and ext build, the two live callers reaching those helpers, and both already call load_composed/load_composed_with_board earlier in the same command, which fires the warning first, so this isn't a live gap in either path today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants