Releases: nirnx/zester
Release list
Zester v0.6.12
Fixed
- Consistent UNREACHABLE behavior: the fast path now fires for all silent
targets (no 30s heartbeat-TTL blind window), missing peels at timeout are
named explicitly with the canonical UNREACHABLE message, and every path
(master fast path, CLI timeout,--direct) produces identical output.
Zester v0.6.11
Fixed
- Consistent text output shape: peel header lines no longer carry inline
status words (UNREACHABLE). Every target now renders identically —
colored name + colon on the header, details indented below — matching the
existing success/failure format.
Zester v0.6.10
Added
- Presence-aware job finalize (UNREACHABLE fast path): at dispatch the master
records which targets have no livepeel-heartbeatentry
(Job.OfflineAtDispatch, additive). The hint NEVER gates delivery — every
resolved target is still published to, and any ack or return overrides it
(the per-job ack is the authoritative delivery proof) — it only bounds the
wait: a grace period (default 5s) after the ack-window re-publish, targets
that were heartbeat-absent AND never acked AND never returned are finalized
with an explicit synthetic per-peel return (UNREACHABLE: no heartbeat at dispatch and no ack after republish,Return.Unreachable, additive),
published on the return subject so the dispatching CLI finishes early. A
ping at a stopped peel now answers in ~10s with an explicit status instead
of burning the full 60s timeout. Recovery watchers keep deadline semantics. - Classified CLI exit codes for module executions (job mode and
--direct):
0all targets succeeded,2one or more peels returned a failed result,
3one or more peels unreachable/missing (incl.--directno-responders),
4both classes at once;1stays the generic CLI/infrastructure error.
Previously a job-mode timeout with missing returns exited0. Text output
gains explicit<peel>: UNREACHABLEstatus lines, JSON/YAML records carry
a per-targetstatusfield (success/failed/unreachable), and
zester job showgains a STATUS column in its Returns table.
Zester v0.6.9
Added
- Peel
startup_states/startup_sls_listconfig knobs (Saltstartup_states
parity): run states ONCE per process start —highstateapplies the full
highstate,slsapplies the listed refs in order (each ref a separate
sequential run). The run goes through the same serialized exec path as
remote executions; online it waits (bounded, 60s grace) for the peel's
FIRST state-file sync so it compiles against current published truth —
never a stale baked fallback tree — while offline boots proceed with the
local tree after the grace (offline-first enforcement preserved). It also
retries with capped backoff while the peel's state infrastructure is still
coming up (escalating to Warn after 10 attempts), so a freshly provisioned
peel converges autonomously right after enrollment (enroll → sync → apply)
with no operator dispatch. Invalid values (unknown mode,slswithout a
list, a list withoutstartup_states: sls) abort startup loudly. Salt's
startup_states: topform is not supported; recurring enforcement stays
with the scheduler (run_on_start+ interval).
Changed
- Salt parity:
state.applywithout a state reference is now an alias for
state.highstate— the full highstate is compiled and applied. The rewrite
lives on the peel's authoritative dispatch path, so every producer (CLI,
REST API, reactor actions, scheduler entries) behaves identically;
previously such requests erroredstate.apply requires 'state' arg. The
peel resolves the state reference fromstate(canonical),mods(Salt's
kwarg name), or the request's state ID, in that order — only a request with
none of them highstates. The CLI treats a key=value-only invocation
(zester '*' state.apply test=true— the classic highstate dry run) as the
highstate form, recognizes ONLY the declared keys (state=,mods=,
test=) as key=value openers (any other first token stays a positional
state name and fails loudly instead of silently escalating to a highstate),
rejects stray positionals after key=value arguments, and gives the bare form
the highstate timeout budget (10m). Mixed fleets degrade loudly, never
silently: an old peel answers a new CLI's bare form with the old per-peel
error, and an old CLI still refuses the bare form client-side.
Fixed
- Reactor
dispatch.state: {sls: ...}now dispatches the named state: it sent
the state reference under an args key (mods) no peel ever read, so every
such reaction failed on the peel withstate.apply requires 'state' arg.
It now sends the canonicalstatekey (old peels honor it too), and new
peels additionally honormodsand ID-only forms (local.state.apply
sugar,dispatch.modulestate_id) from not-yet-upgraded masters — without
either fix those reactions would have silently become full highstates under
the new alias.
Zester v0.6.8
Added
- Colored module documentation in the terminal:
sys.docreplies and the
offlinezester docrender (module, family, and index forms) are colorized
client-side over the pinned plain-text grammar — module headers, section
headings, parameter names/flags (requiredyellow,sensitivered), effects,
examples, and notes by severity. The wire format andRenderTextstay
byte-identical (stripping the ANSI codes reproduces the plain render exactly,
test-pinned); color follows the existing TTY +NO_COLOR+--no-color
rules.cmd.runand other module output is never rewritten. - New global CLI flag
--force-color: force colored output when stdout is not
a TTY (piping toless -R, CI captures). An explicit request, so it overrides
theNO_COLORenv;--no-colorstill always wins.
Zester v0.6.7
Security
- Dependency review: bumped
postcssto 8.5.19 in the docs website (fixes a
CSS-stringify XSS advisory) alongside the fumadocs/lucide/@types minors, and
sigstore-goto 1.2.0 (fixes a multi-log threshold-bypass advisory;
integration-test-only dependency). The TypeScript 7 major was declined — it
breaks the Next.js 16 site build — and the remaining docker/docker advisories
are integration-test-only transitives with no patched release yet.
Changed
- Module reference pages are now one page per FAMILY (Salt-style tree).
modules → pkgrenders everypkg.*state on a single page: a function
index up top, a Family Parameters section rendered once from the shared
family components (with per-member tables for member-supplied dimensions —
e.g.modedefaults 0644/0755 — and exposure lists when not every member
takes a parameter), then per-member sections with stable anchors
(/docs/guides/modules/file#file-managed). 44 per-module pages collapse
into 19 family pages; docgen deletes stale generated pages automatically.
Notes and Divergences (BD ids) are no longer rendered on the website pages
— they remain in the moduleDocfor spec bookkeeping and in the terminal
sys.doc/zester docoutput.
Zester v0.6.5
Added
-
Family parameter components (Amendment A1, spec §13). The
file.*
family's canonical parameters are now declared ONCE in embeddable family
components —makedirs,mode,user/groupownership, andsource—
and members embed them; the schema declaration, canonical usage text, and
runtime behavior contract are all shared, per the approved family-scoped
contract model. The framework gains member-supplied dimensions
(memberdefault/memberrequiredtag options +modschema.WithDefault/
WithRequired): the mode component fixes name/type/semantics while
file.managedsupplies 0644 andfile.directory0755; the source
component fixes everything but requiredness (file.copyrequires it).
Every field is stamped with its declaring type, and the vocabulary gate
gains a COMPONENT RATCHET: once a family component exists for a key, a
private redeclaration in any member — even byte-identical — fails CI. -
Paired-family components (A1 step 7).
ssh_auth.*(the user/config
target pair),host.*(the config/path hosts-file selector),cron.*
(the user/command entry identity), andpkg.*(refresh, with
member-supplied defaults:pkg.installedfalse,pkg.latesttrue —
behavior unchanged) now declare their canonical parameters once in family
components.git.*'s shared keys (branch/rev/force) stay member-declared:
their per-operation semantics genuinely differ, and the gate keeps their
contract signatures aligned. The pkg refresh vocabulary-ratchet entry is
retired — the in-family exception table is down to the single
maintainer-blessed permanent (file.line's action-selectormode). -
Canonical
file.*makedirsruntime contract, enforced by a shared
behavior suite. All six members (managed,copy,directory,
recurse,symlink,touch) now implement ONE contract:makedirs
governs missing PARENTS of the target only (created at 0755 when true);
when false, a missing parent fails BOTH Check and Apply with an error
naming the parent and themakedirs: trueremedy — no partial creation,
and Revert never removes created parents. A four-case behavior suite
(false/true × parents present/missing) runs against every embedding member. -
pathalias unified across the whole file family. All 13file.*
modules now acceptpathas an alias of their primary — previously 7 did
and 6 (absent, append, blockreplace, directory, recurse, symlink) rejected
it under strict params (or, on blockreplace, deliberately ignored it), so
- path: /srv/wwwworked onfile.managedbut failed onfile.directory.
Pinned by three-universe contract fixtures per module; blockreplace's old
"path is not an alias" pin is retired by this change. -
Family form for the doc surfaces (Salt parity).
zester '<target>' sys.doc ssh_authand the offlinezester doc ssh_authnow render every
documentedssh_auth.*module as one document (sorted, shared
RenderTextAllshape on both surfaces) instead of erroring
no documentation for "ssh_auth". Works for any family —file,pkg,
facts(dispatch specials included);--jsonreturns an array for a
family. Family names also joinzester doc's did-you-mean suggestions and
shell completion. -
Parameter vocabulary gate. A conformance test
(TestParameterVocabularyConsistency) now compares every parameter key —
canonical names and aliases — across ALL state and exec modules and fails
when the same key carries different schemas ("modemust always behave the
same"). The fleet passes with exactly three documented Salt-parity
exceptions (modein file.line = action selector;gidin group.present =
numeric-only create id;textin test.echo = scalar echo string), each
justified in the exception table, which also refuses stale entries. The
developing guide documents the rule. REWORKED to the family-scoped
two-tier form per the approved Amendment A1 (spec §13): the contract
boundary is (module kind, family, parameter name) — within one family the
full contract is compared (shape, aliases, primary, requiredness,
default), across families only the value shape (the same spelling may
legitimately mean different things in unrelated families). Alias keys are
first-class: the gate caughtdir_modemeaning an alias-of-mode on
file.directory but a standalone parameter on file.recurse. In-family
divergences are pinned as migration-ratchet entries the family-component
tranches delete. Exceptions are participant-pinned:
the exception covers only the known divergence, so a NEW module reusing an
excepted key (e.g. a thirdmodeshape) still fails until the pin is
deliberately updated. Dual-surface modules (cmd.run) now carry their
"also reachable as an execution module" header in the OFFLINE docs too —
the embedded docdata gains the same AlsoExecmod overlay live sys.doc
renders, and the docdata↔live parity test covers it.
Changed
- Family-oriented package layout (A1 step 8).
pkg/state/modulesis now
one package per module family (modules/file/,modules/cron/, …; package
namesfilemod,cronmod, …), each holding its members, its parameter
components (components.go), its behavior suites, its contract fixtures,
and its registration rows; shared plumbing lives inmodules/regdef
(registration types +MustSpec) andmodules/internal/famshared
(cross-family helpers). The aggregatorpkg/state/moduleskeeps its import
path and public API (RegisterAll, the dispatch specials) unchanged — no
consumer changes; file history follows the moves.
Fixed
-
makedirs: trueno longer accepts a regular file at the parent path, and
non-ENOENTStatfailures are surfaced instead of masked. The Apply-side
parent-creation arm returned success wheneverStat(parent)succeeded —
without checking it was a directory — and fell through toMkdirAllon ANY
stat error, hiding permission/I-O/provider failures behind a creation
attempt. It now succeeds only for an existing directory, reports a clear
not-a-directory error for a file, creates only on a genuine not-exist, and
wraps every other stat error untouched. Two new arms in the shared family
behavior suite pin both cases per member (regular-file parent; injected
non-ENOENT stat failure via the fake's new fault injection). -
makedirsCheck semantics re-ruled (Salt-aligned). A missing parent
withmakedirsunset no longer fails Check: dry runs do not materialize
changes from earlier required states, so a correctly ordered tree (a
directory state creating the parent, a file state writing into it with
require) must dry-run clean — Check now reports a would-change whose
detail names the missing parent and the remedy. Apply retains the strict
canonical contract: a parent still missing when the operation actually runs
fails without partial creation. Pinned by the ordered-tree dry-run,
ordered-tree apply, and standalone strict-apply cases in the family
behavior suite; BD-9's wording updated. (Also fixed en route: the exectest
fake's Chmod wiped file-TYPE bits, silently un-directorying entries.) -
A1 final-verification round.
file.directory's documentation was
drift-corrected (it still described the pre-fix inertmakedirs; the
published page contradicted its own parameter table) and the five other
members' Check effects now document the missing-parent failure; the fix is
minted as BD-9 in the spec and each member's Divergences. The makedirs
helpers nowCleanthe target path first (a trailing-slash target no
longer gates on itself) and report a parent that exists as a regular file
with a clear not-a-directory error.pkg.*'srefreshwas
UN-componentized: its two members genuinely differ in phase coverage and
error semantics (installed: Apply-only, fatal; latest: Check+Apply,
warn-only), so per §13 it is member-declared with a permanent pinned
exception — the git.* precedent. Gate hardening: the component ratchet now
fires for single-embedder components (a member defecting from a 2-member
component was previously invisible); member-supplied dimensions are only
legal on embedded component fields; untagged pointer-to-struct embeds are
compile errors (previously silently dropped the whole component); the
declaring-type stamp gained direct tests; same-signature keys with
per-member runtime meanings (dir_mode) are declared in a participant-
pinned meaning-variance registry. The behavior suite now asserts the 0755
creation mode, that errors name the missing parent, revert-never-removes-
parents, trailing-slash handling, and pins suite completeness against the
live embedder set. -
file.directorymakedirswas accepted but inert — deliberate
compatibility fix (A1). The module previously always created the full
parent chain (MkdirAll), silently masking typos in deep paths, and
ignored the flag. It now follows the canonical contract: a missing parent
withoutmakedirs: truefails Check and Apply. Behavioral difference from
0.6.0, maintainer-ruled; pinned by the shared behavior suite and the
makedirs contract fixtures across YAML/CLI/msgpack. -
The other five
makedirsmembers failed missing-parent cases with raw
OS errors and reported phantom applicable changes in Check. All now fail
both phases with the canonical contract error.file.recursepreviously
created missing parents withdir_modeinstead of the canonical 0755. -
file.directory'sdir_modeis now a standalone parameter (was an
alias ofmode): decode lands on its own field and the mode fallback
(mode wins;dir_modefills in whenmodeis undeclared) moved to the
builder — behavior is byte-identical to the alias era, pinned by
TestFileDirectoryDirModeFallbackand updated contract fixtures; its
contract signature now matchesfile.recurse's `...
Zester v0.6.0
Added
-
sys.doc— on-node module documentation.zester '<target>' sys.doc
returns a unified index of every callable surface (state modules, execution
functions, and the peel dispatch specialsstate.apply/state.highstate/
facts.*/settings.*/pillar.*/event.send);sys.doc <module>renders
that module's documentation through the samemodschema.RenderTextused by
zester docand the generated pages, mirroring dispatch precedence
(dispatch specials → state registry → execution registry). A module that is
both a state and an execution surface (e.g.cmd.run) is annotated as also
reachable from templates viasalt['<module>'].sys.docruns on the peel's
read-only fast path, so it answers even during a long highstate; its result
rides the existingExecResponseresult field (no wire change). -
sys.list_functionsnow lists every callable surface on the peel — state
modules and dispatch specials in addition to execution functions (it
previously listed only execution functions). -
Self-documenting execution modules + the first execution-module reference
page. Every built-in remote-execution function (test.echo/version/true/
false,pkg.version/list_pkgs,service.status/start/stop/restart,
disk.usage,cmd.run,grains.item/items,sys.list_functions,
sys.doc) now carries amodschema.Spec(Kindexec), registered via
Registry.RegisterSpec— sosys.doc <function>,zester doc, and the
generated docs all describe it with drift-corrected parameter and behavior
metadata. Parameter schemas are lifted from each function's actual argument
alias sets (e.g.pkg.versionacceptsname/package/pkg, defaulting to
the request ID) in the SAME precedence order the runtimeargStrhelper
checks them —cmd.runas an execution module (salt['cmd.run'](...),
zester '<target>' cmd.run ...) declarescmdas its canonical primary
(aliasescommand,name, argStr's exact check order), notcommand—
a declared canonical name is checked before its aliases, so a spec whose
order didn't match the runtime's own precedence would silently accept the
wrong value when a caller set more than one of them; pinned by a permanent
contract fixture where bothcmdandcommandare set. (This deliberately
differs from thecmd.runSTATE module, whose primary iscommand— a
different surface with its own precedence, see BD-8 below.) A new combined
reference page,
Execution Modules, documents them all
(generated byzester-docgen;cmd.runstays on its dual-surface state
page). A closed execmod doc-coverage conformance gate now requires every
registered execution function to carry a spec, and permanent contract fixtures
pin the argument-alias decoding across YAML/CLI/msgpack. -
zester doc [module] [--json]— offline operator documentation for
self-documenting modules (Track B2). Renders the embedded module docs
(pkg/moduledoc) through the SAMEmodschema.RenderTexta connected daemon
uses forsys.doc, so the offline CLI answer is byte-identical to the
peel-side one (pinned byTestDocdataMatchesLive). Fully offline — reads only
the embedded docdata, never contacts a master, and works on a peel-only box
with no config. Barezester doclists documented modules grouped by family;
zester doc <module>prints the full parameter/effects/examples/notes render;
an unknown module reports a clear error with nearest-match suggestions (edit
distance ≤ 2);--jsonemits the structuredModuleInfo. Cobra shell
completion now suggests documented module names for bothzester doc <TAB>
and the exec formzester '<target>' <TAB>(sourced frommoduledoc.All()).
The CLI's streamlined-output set gainssys.docso a peel-sidesys.doc
result prints as plain rendered text. Internally,parseModuleArgsnow binds
a bare positional to a self-documenting module's declared primary parameter
looked up from the embedded docdata (replacing the hand-maintained per-module
positional table); every previously hard-coded module resolves identically,
andfile.managed's positional now binds to its canonical primaryname
(decode-identical to the formerpath, which is a registered alias, with the
ID carrying the same value). Modules absent from docdata keep the legacy
fallback (mixed-fleet honesty). -
Self-documenting Starlark custom modules. The
.starmodule loader now
captures each module's documentation at load time — the function docstring
(Google-style summary/description +Args:section, via the pinned
go.starlark.netFunction.Doc()), the source location (Function.Position()),
and an optional per-function<fn>_paramsor module-globalPARAMSdict — and
registers amodschema.Specso Starlark modules appear insys.doc/zester docthrough the state Registry'sDescribeexactly like built-in Go modules.
A module is registered withOpenParams(accepts arbitrary keys) unless it
declares aPARAMS/<fn>_paramsdict; a declared dict opts the module into
unknown-key validation, which surfaces unknown config keys through the peel's
configured decode policy (LoaderConfig.DecodeOptions) while leaving the
requisite/attribute/compiler directives andnamereserved. Documentation is
re-captured on hot-reload. Authoring convention documented in the hand-owned
guides/modules/starlark.mdx. -
Generated JSON Schema artifact for module parameters
(website/public/schema/zester-modules.schema.json, JSON Schema draft
2020-12) — machine-readable parameter schemas for every state module that
has a migrated self-documenting schema (file.managed,file.directory,
file.absent,file.append,file.copy,file.recurse,file.symlink,
file.touch,file.line,file.replace,file.comment,file.uncomment,
file.keyvalue,file.blockreplace,pkg.installed,pkg.latest,
pkg.purged,pkg.removed,service.running,service.dead, and
user.present) —service.running/service.deadcontribute the shared
TriStatesemantic-type$def,file.managedtheTemplateFlagand
FileMode$defs,user.presenttheGroupRefandStringList$defs,
file.appendis the second module to contribute to theStringList$def,
file.keyvaluecontributes theStringMap$def(itskey_valuesand
entriesparameters — the type's first migrated consumer), and
file.directory/file.recurseare furtherFileMode$defcontributors
(file.directory'smodewith adir_modefallback alias;file.recurse's
DECLARED-ONLYdir_modeand lazyfile_mode); a module without a schema
is left unconstrained, so the artifact
grows automatically as more modules migrate, never breaking existing
consumers. Generated deterministically by the newcmd/zester-docgentool
from the live module registries. -
New architecture page: Self-Documenting Modules.
A hand-owned overview, for contributors and operators, of the module-schema
framework introduced above: how one compiled schema declaration per module
(a tagged struct plus aDocblock) drives parsing, the strict unknown-
parameter policy,sys.doc/zester doc, the generated reference pages, and
the JSON Schema export from a single source of truth; the sealed six-type
semantic vocabulary; permanent contract fixtures as behavioral regression
guards across YAML/CLI/msgpack; and the concrete gates (the docgen
freshness diff, the closed doc-coverage ratchet,TestDocdataMatchesLive,
the package-boundary architecture test) that make documentation drift a
build failure instead of a silent fact of life. Registered in the
Architecture nav (architecture/meta.json). -
New guide: Developing a Built-in Module (
guides/modules/developing) —
the step-by-step howto for adding a state module with a self-documenting
schema: the proto struct andzestertag grammar, semantic types, the
drift-correctedDoc, builder/lifecycle contract, registration and the
coverage gates, contract fixtures across the three input universes, docgen
regeneration, and the exec-module variant. Complements the operator-facing
Starlark guide and the architecture page.
Changed
-
Starlark hot-reload now UNREGISTERS removed modules. A function removed
from a reloaded.starfile — and every module of a deleted.starfile or
a removed_modules/directory — is unregistered from the state registry on
the next load pass, so it is neither callable nor documented (previously the
stale builder and docs survived until a peel restart). When another loaded
file still provides the same module name (a deleted formula override with a
surviving global definition), that surviving file is re-executed so ITS
builder becomes live again. Newstate.Registry.Unregisterseam backs this;
a conformance test pins that built-ins are never unregistered. -
A
.starmodule can no longer shadow a built-in module name. The
Starlark loader refuses to register a module whose name is already held by a
non-Starlark registration (e.g. apkg.stardefininginstalledcolliding
with the built-inpkg.installed) and logs an error naming the file —
previously the.starsilently hijacked the built-in fleet-wide with no way
to restore it until restart. Starlark-over-Starlark overrides (formula
overrides global, hot-reload) are unaffected. -
A typo'd state-module parameter now FAILS the build instead of being silently
ignored (strict_params, default on). The peel gains astrict_paramsknob
(peel.yamlstrict_params:/--strict-params, default true). With every
built-in state module now decoding through a compiled s...
Zester v0.5.0
Added
- Release promotion + auto-rollout.
zester update promotemarks a
published version as a promoted release: it never expires, and masters
automatically roll the fleet to the latest promoted version through the
full existing rollout machinery (batches, soak, auto-rollback, failure
budget, degraded-node exclusion) — promotion is now the one-command
release act. Auto-rollout never downgrades (nodes at or ahead of the
promoted version are untouched), starts at most one rollout per component
at a time, and uses deterministic generation ids
(rol-auto-<component>-<version>-r<N>) so racing masters CAS-conflict
instead of double-rolling. A COMPLETED run never blocks convergence: a
node that was offline during the rollout (or enrolled later) still lags
and gets the next generation, targeting exactly the laggards; an
operator-ABORTED auto-rollout, by contrast, is never retried
automatically for that version. The fleet switch is stored in NATS and on by default —
safe, because nothing rolls until something is explicitly promoted — and
flippable at runtime withzester update auto on|off|status(no master
restart). Per-master participation and tuning viaupdate_auto_rollout
(default true),update_auto_components(default peel),
update_auto_batch_size(5),update_auto_soak_time(60s),
update_auto_max_failed(1),update_auto_interval(1m). - Per-version TTLs replace the object-store bucket TTL. Every published
version now carries its own expiry in the manifest:update publish --ttl <d>(default 30 days,0= never),update set-ttladjusts it
later,update demoteresumes expiry for a formerly promoted version.
A lease-gated hourly GC on the master reaps expired versions (object +
manifest) — skipping anything a non-terminal rollout still references —
and sweeps manifest-less orphan objects. Theupdate-binariesbucket TTL
is removed (it would have deleted promoted binaries behind the GC's
back); versions published by older CLIs keep their original
published + 30dlifetime. zester update rolloutslists rollout history (id, component,
version, state, batch progress, failure budget, timestamps; newest
first) — rollout ids no longer live only in the start command's output.zester update unpublishremoves a published version (binary +
manifest); refused while a non-terminal rollout references it, and
promoted versions require--force. Node-side rollback is unaffected
(nodes revert from their local previous-binary slot).zester update versionsnow shows PROMOTED and EXPIRES columns and
sorts newest-first.
Changed
zester update status --rolloutNode Results are stable-sorted by
batch, then node name, with a new BATCH column — consecutive invocations
line up row-for-row instead of shuffling with map iteration order.
Zester v0.4.2
Fleet-wide state-module convergence audit. After the pkg.latest
stale-index bug shipped in the field, every state module (44 states, 36
files) was audited against the Check/Apply contract; 48 adversarially
verified defects across 25 states are fixed in this release. Three defect
classes dominated: Check blind to a facet only Apply enforces (drift never
converges, reported compliant forever), destructive or dishonest Revert
paths, and read errors conflated with "file absent" (data-loss paths).
Fixed
pkg.latestrefreshes the package cache BEFORE checking upgradability
(Salt parity). Refresh (default on) ran only in Apply, but Check consulted
the stale index and short-circuited "already at latest" — so Apply, and
with it the refresh, never executed: any release published after the box's
last cache refresh was invisible (field symptom: the first fleet-wide
pkg.latest zester-peelafter the 0.4.1 repo publish was a silent
changed: 0no-op on every apt host). Check and Apply now each run their
own refresh independently. A FAILED refresh (one rotted third-party repo
failsapt-get updatewhile reachable repos still updated) warns and
proceeds in both phases instead of failing the state;--testdry runs
refresh the index too (metadata-only).pkg.installedhonors a declaredversion:pin in Check. Any
installed version used to satisfy a pinned state, so version drift was
compliant forever. Check now compares the installed version against the
pin (got/want diff). Pinned downgrades converge on every provider: apt
passes--allow-downgrades, yum verifies the pin landed and falls back
toyum downgrade(plainyum install pkg-<older>silently no-ops),
dnf handles explicit version downgrades natively. Undeclaredversion:
is unchanged.pkg.purgedtreats a removed-but-not-purged Debian package (dpkgrc
state) as needing a purge — converged only when no package record
exists at all; previouslyrcreported converged and leftover conffiles
were never removed. The apt probes also fail loudly when the probe never
ran (spawn failure/context death) instead of reporting "not installed",
and parse multi-arch dpkg output correctly.cron.present,sysctl.present, andmount.mountednow actually work
on real peels. Their exec providers (crontab/procfs/fstab) were never
wired into provider detection — every real-peel run failed with "no
cron/sysctl/mount provider available" (unit tests wired providers
manually, masking it).file.managedenforces themode:facet on pre-existing files.
Apply wrote content via a create-time-perm-only write and never chmodded,
so mode drift on an existing file was reported by Check forever but never
fixed — permanent churn firingwatchdependents every highstate. Apply
now chmods after writing; Revert restores the CAPTURED prior mode, not
the desired one. Octal modes with setuid/setgid/sticky digits
("4755", "1777") are translated to the Go FileMode flags — previously the
special bit was silently dropped by chmod and the state re-applied forever
without ever setting it — and ownership is applied BEFORE mode (chown on
an executable clears setuid). Applies to file.managed, file.directory,
and file.recurse modes.- Debian
rc-state packages (removed, conffiles remain) no longer count
as installed. The apt probe requires dpkg statusinstalled
(dpkg-query -W -f='${db:Status-Status}') instead of thedpkg -sexit
code —pkg.installedcould never reinstall a previously-removed
conffile-bearing package, andpkg.removedre-applied (and firedwatch
dependents) on every highstate after its own successful removal. pkgrepo.managedverifies the Debian signing key. Thekey_urlkey
now lands persistently at/etc/apt/keyrings/zester-<name>.gpg(was a
volatile/tmpdownload) and Check reports drift when it is missing —
previously a never-imported/deleted key leftapt-get updatefailing
NO_PUBKEY while the state reported converged. Presence-only (in-place key
rotation at the same URL is not detected); Revert removes the artifact.- File states detect ownership drift.
file.managed,file.directory,
andfile.recursecompare on-disk owner/group against declared
user:/group:in Check (declared facets only — undeclared ownership
never churns).file.recurseCheck also flagsclean: trueextra files
(previously the clean feature could never fire once the managed set
converged) anddir_modedrift on every managed dir, and all its
filesystem walks now go through the injected file provider
(newFileExec.Walk). service.running/service.deadcompare a declaredenable:facet in
Check — running-but-disabled (enable: true) and stopped-but-enabled
(enable: false, resurrects at reboot) were compliant forever.
service.runningwithenable: falsenow actually disables; an Apply
reached only for enable drift never restarts the running service.user.presentconverges password and name-based primary group. A
declaredpassword:hash is compared against the shadow hash (new
UserExec.PasswordHash); a name-basedgid:/primary_group:is compared
and enforced on existing users viausermod -g— both facets were
silently unenforced outside user creation.mount.mountedfstab comparison now includesdump/pass, Apply
no-ops honestly when converged, and Revert no longer claims an unmount it
never performed. (Comparing the LIVE mount — a wrong device/options
serving the mountpoint — is deferred: it needs Salt-style
option/fstype/device normalization to avoid remount churn on
kernel-normalized values; that audit finding stays open on the backlog.)sysctl.presentwithpersist: trueverifies the drop-in file entry,
not just the runtime value — a manualsysctl -wmatch silently died at
the next reboot.cron.presententries are keyed on the label (identifier comment,
Salt semantics) instead of the exact command string — editing a state's
command replaces the old line instead of orphaning it to run forever.
Managed entries carry a# ZESTER_CRON_ID: <label>marker; pre-existing
same-command lines are adopted (a human descriptive comment neither
blocks adoption nor duplicates the job) and stamped on the next apply.
Converged Apply is a no-op, so watch-forced runs no longer rewrite the
crontab. Crontab edits are LINE-PRESERVING: Set/Remove splice only
the targeted entry and its marker — MAILTO=/PATH= environment lines,
human comments, blank lines, and @reboot/@daily nickname entries survive
untouched (the previous whole-crontab reconstruction silently destroyed
them), and a Remove that matches nothing does not rewrite the crontab at
all. Commands are compared whitespace-normalized, so a declared command
with consecutive spaces converges instead of rewriting every run.git.cloned/git.latestwith a symbolicrev:(tag) converge —
the rev was compared against the HEAD sha by string prefix, which never
matches a tag name, so every run re-applied (needless fetches,watch
cascades, service restarts every highstate). Symbolic revs resolve locally
viagit rev-parse --verify <rev>^{commit}; sha prefixes still match
directly.locale.presentverifies the/etc/locale.genenabling line that
Apply writes, not justlocale -amembership — an out-of-band-generated
locale was compliant until the nextlocalespackage upgrade silently
dropped it. The facet applies only where/etc/locale.genexists
(Debian-family); RHEL/musl systems stay satisfied bylocale -a, no
straylocale.genis created, and charmap spellings are normalized
(en_US.utf8==en_US.UTF-8).- Read errors are no longer conflated with "file absent" anywhere.
host.present,ssh_auth.present, every text-editing file state
(file.line/append/blockreplace/comment/keyvalue/replace),
file.managed/copy/recurse,cmd.run'screatesprobe,
locale.present, and the archive marker all treated ANY read error as
"missing" — a transient EIO/EACCES/ESTALE followed by a successful write
could truncate/etc/hostsor a user'sauthorized_keysdown to the one
managed line, or clobber file content that was never captured. Only
fs.ErrNotExistselects the absent path now; anything else fails the
phase without writing. - Revert contract: a fresh instance never destroys state. Revert
consuming in-instance memos (backups, created flags, saved originals)
treated "memo unset" as "file was new" and DELETED the target — and the
runner builds FRESH instances for revert, so a revert run would have wiped
/etc/hosts,authorized_keys, or any managed file. Fresh-instance
Revert is now an explicit clean no-op across ALL modules
("nothing to revert (no apply recorded in this run)"); same-instance
Apply→Revert still restores backups (with canonical permissions —
authorized_keysrestores 0600) and still removes files the same
instance created. (tolerating an already-externally-removed file). Related honesty fixes:sysctl.presentRevert no
longer writes an EMPTY value into the kernel and persist file;
mount.mountedRevert no longer claims an unmount it never performed;
group.presentRevert really restores membership (it only restored the
GID);service.runningRevert reports enable-reverts. - Watch-forced applies are safe on guarded/absent states.
cmd.run's
createsguard now also gates Apply (a watch trigger used to re-run
creates-guarded one-shots likeinitdb);user.absent/group.absent
Apply no-op cleanly when already absent instead of failinguserdel/
groupdel;service.dead/mount.mounted/sysctl.present/user.present
Apply can now honestly reportchanged: falsewhen converged.
archive.extractedApply re-evaluates its `i...