Skip to content

Releases: nirnx/zester

Zester v0.6.12

Choose a tag to compare

@github-actions github-actions released this 15 Jul 23:54

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

Choose a tag to compare

@github-actions github-actions released this 15 Jul 23:05

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

Choose a tag to compare

@github-actions github-actions released this 15 Jul 21:56

Added

  • Presence-aware job finalize (UNREACHABLE fast path): at dispatch the master
    records which targets have no live peel-heartbeat entry
    (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):
    0 all targets succeeded, 2 one or more peels returned a failed result,
    3 one or more peels unreachable/missing (incl. --direct no-responders),
    4 both classes at once; 1 stays the generic CLI/infrastructure error.
    Previously a job-mode timeout with missing returns exited 0. Text output
    gains explicit <peel>: UNREACHABLE status lines, JSON/YAML records carry
    a per-target status field (success/failed/unreachable), and
    zester job show gains a STATUS column in its Returns table.

Zester v0.6.9

Choose a tag to compare

@github-actions github-actions released this 14 Jul 20:39

Added

  • Peel startup_states / startup_sls_list config knobs (Salt startup_states
    parity): run states ONCE per process start — highstate applies the full
    highstate, sls applies 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, sls without a
    list, a list without startup_states: sls) abort startup loudly. Salt's
    startup_states: top form is not supported; recurring enforcement stays
    with the scheduler (run_on_start + interval).

Changed

  • Salt parity: state.apply without 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 errored state.apply requires 'state' arg. The
    peel resolves the state reference from state (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 with state.apply requires 'state' arg.
    It now sends the canonical state key (old peels honor it too), and new
    peels additionally honor mods and ID-only forms (local.state.apply
    sugar, dispatch.module state_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

Choose a tag to compare

@github-actions github-actions released this 14 Jul 16:55

Added

  • Colored module documentation in the terminal: sys.doc replies and the
    offline zester doc render (module, family, and index forms) are colorized
    client-side over the pinned plain-text grammar — module headers, section
    headings, parameter names/flags (required yellow, sensitive red), effects,
    examples, and notes by severity. The wire format and RenderText stay
    byte-identical (stripping the ANSI codes reproduces the plain render exactly,
    test-pinned); color follows the existing TTY + NO_COLOR + --no-color
    rules. cmd.run and other module output is never rewritten.
  • New global CLI flag --force-color: force colored output when stdout is not
    a TTY (piping to less -R, CI captures). An explicit request, so it overrides
    the NO_COLOR env; --no-color still always wins.

Zester v0.6.7

Choose a tag to compare

@github-actions github-actions released this 14 Jul 15:28

Security

  • Dependency review: bumped postcss to 8.5.19 in the docs website (fixes a
    CSS-stringify XSS advisory) alongside the fumadocs/lucide/@types minors, and
    sigstore-go to 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 → pkg renders every pkg.* 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. mode defaults 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 module Doc for spec bookkeeping and in the terminal
    sys.doc/zester doc output.

Zester v0.6.5

Choose a tag to compare

@github-actions github-actions released this 14 Jul 03:59

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/group ownership, and source
    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/memberrequired tag options + modschema.WithDefault/
    WithRequired): the mode component fixes name/type/semantics while
    file.managed supplies 0644 and file.directory 0755; the source
    component fixes everything but requiredness (file.copy requires 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), and pkg.* (refresh, with
    member-supplied defaults: pkg.installed false, pkg.latest true —
    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-selector mode).

  • Canonical file.* makedirs runtime 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 the makedirs: true remedy — no partial creation,
    and Revert never removes created parents. A four-case behavior suite
    (false/true × parents present/missing) runs against every embedding member.

  • path alias unified across the whole file family. All 13 file.*
    modules now accept path as 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/www worked on file.managed but failed on file.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_auth and the offline zester doc ssh_auth now render every
    documented ssh_auth.* module as one document (sorted, shared
    RenderTextAll shape on both surfaces) instead of erroring
    no documentation for "ssh_auth". Works for any family — file, pkg,
    facts (dispatch specials included); --json returns an array for a
    family. Family names also join zester 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 ("mode must always behave the
    same"). The fleet passes with exactly three documented Salt-parity
    exceptions (mode in file.line = action selector; gid in group.present =
    numeric-only create id; text in 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 caught dir_mode meaning 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 third mode shape) 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/modules is now
    one package per module family (modules/file/, modules/cron/, …; package
    names filemod, cronmod, …), each holding its members, its parameter
    components (components.go), its behavior suites, its contract fixtures,
    and its registration rows; shared plumbing lives in modules/regdef
    (registration types + MustSpec) and modules/internal/famshared
    (cross-family helpers). The aggregator pkg/state/modules keeps its import
    path and public API (RegisterAll, the dispatch specials) unchanged — no
    consumer changes; file history follows the moves.

Fixed

  • makedirs: true no longer accepts a regular file at the parent path, and
    non-ENOENT Stat failures are surfaced instead of masked.
    The Apply-side
    parent-creation arm returned success whenever Stat(parent) succeeded —
    without checking it was a directory — and fell through to MkdirAll on 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).

  • makedirs Check semantics re-ruled (Salt-aligned). A missing parent
    with makedirs unset 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 inert makedirs; 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 now Clean the 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.*'s refresh was
    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.directory makedirs was 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
    without makedirs: true fails 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 makedirs members 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.recurse previously
    created missing parents with dir_mode instead of the canonical 0755.

  • file.directory's dir_mode is now a standalone parameter (was an
    alias of mode): decode lands on its own field and the mode fallback
    (mode wins; dir_mode fills in when mode is undeclared) moved to the
    builder — behavior is byte-identical to the alias era, pinned by
    TestFileDirectoryDirModeFallback and updated contract fixtures; its
    contract signature now matches file.recurse's `...

Read more

Zester v0.6.0

Choose a tag to compare

@github-actions github-actions released this 13 Jul 20:05

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 specials state.apply/state.highstate/
    facts.*/settings.*/pillar.*/event.send); sys.doc <module> renders
    that module's documentation through the same modschema.RenderText used by
    zester doc and 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 via salt['<module>']. sys.doc runs on the peel's
    read-only fast path, so it answers even during a long highstate; its result
    rides the existing ExecResponse result field (no wire change).

  • sys.list_functions now 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 a modschema.Spec (Kind exec), registered via
    Registry.RegisterSpec — so sys.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.version accepts name/package/pkg, defaulting to
    the request ID) in the SAME precedence order the runtime argStr helper
    checks them
    cmd.run as an execution module (salt['cmd.run'](...),
    zester '<target>' cmd.run ...) declares cmd as its canonical primary
    (aliases command, name, argStr's exact check order), not command
    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 both cmd and command are set. (This deliberately
    differs from the cmd.run STATE module, whose primary is command — a
    different surface with its own precedence, see BD-8 below.) A new combined
    reference page,
    Execution Modules, documents them all
    (generated by zester-docgen; cmd.run stays 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 SAME modschema.RenderText a connected daemon
    uses for sys.doc, so the offline CLI answer is byte-identical to the
    peel-side one (pinned by TestDocdataMatchesLive). Fully offline — reads only
    the embedded docdata, never contacts a master, and works on a peel-only box
    with no config. Bare zester doc lists 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); --json emits the structured ModuleInfo. Cobra shell
    completion now suggests documented module names for both zester doc <TAB>
    and the exec form zester '<target>' <TAB> (sourced from moduledoc.All()).
    The CLI's streamlined-output set gains sys.doc so a peel-side sys.doc
    result prints as plain rendered text. Internally, parseModuleArgs now 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,
    and file.managed's positional now binds to its canonical primary name
    (decode-identical to the former path, 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 .star module loader now
    captures each module's documentation at load time — the function docstring
    (Google-style summary/description + Args: section, via the pinned
    go.starlark.net Function.Doc()), the source location (Function.Position()),
    and an optional per-function <fn>_params or module-global PARAMS dict — and
    registers a modschema.Spec so Starlark modules appear in sys.doc / zester doc through the state Registry's Describe exactly like built-in Go modules.
    A module is registered with OpenParams (accepts arbitrary keys) unless it
    declares a PARAMS/<fn>_params dict; 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 and name reserved. 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.dead contribute the shared
    TriState semantic-type $def, file.managed the TemplateFlag and
    FileMode $defs, user.present the GroupRef and StringList $defs,
    file.append is the second module to contribute to the StringList $def,
    file.keyvalue contributes the StringMap $def (its key_values and
    entries parameters — the type's first migrated consumer), and
    file.directory/file.recurse are further FileMode $def contributors
    (file.directory's mode with a dir_mode fallback alias; file.recurse's
    DECLARED-ONLY dir_mode and lazy file_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 new cmd/zester-docgen tool
    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 a Doc block) 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 and zester tag grammar, semantic types, the
    drift-corrected Doc, 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 .star file — and every module of a deleted .star file 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. New state.Registry.Unregister seam backs this;
    a conformance test pins that built-ins are never unregistered.

  • A .star module 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. a pkg.star defining installed colliding
    with the built-in pkg.installed) and logs an error naming the file —
    previously the .star silently 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 a strict_params knob
    (peel.yaml strict_params: / --strict-params, default true). With every
    built-in state module now decoding through a compiled s...

Read more

Zester v0.5.0

Choose a tag to compare

@github-actions github-actions released this 10 Jul 15:25

Added

  • Release promotion + auto-rollout. zester update promote marks 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 with zester update auto on|off|status (no master
    restart). Per-master participation and tuning via update_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-ttl adjusts it
    later, update demote resumes 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. The update-binaries bucket TTL
    is removed (it would have deleted promoted binaries behind the GC's
    back); versions published by older CLIs keep their original
    published + 30d lifetime.
  • zester update rollouts lists 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 unpublish removes 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 versions now shows PROMOTED and EXPIRES columns and
    sorts newest-first.

Changed

  • zester update status --rollout Node 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

Choose a tag to compare

@github-actions github-actions released this 10 Jul 05:49

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.latest refreshes 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-peel after the 0.4.1 repo publish was a silent
    changed: 0 no-op on every apt host). Check and Apply now each run their
    own refresh independently. A FAILED refresh (one rotted third-party repo
    fails apt-get update while reachable repos still updated) warns and
    proceeds in both phases instead of failing the state; --test dry runs
    refresh the index too (metadata-only).
  • pkg.installed honors a declared version: 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
    to yum downgrade (plain yum install pkg-<older> silently no-ops),
    dnf handles explicit version downgrades natively. Undeclared version:
    is unchanged.
  • pkg.purged treats a removed-but-not-purged Debian package (dpkg rc
    state) as needing a purge
    — converged only when no package record
    exists at all; previously rc reported 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, and mount.mounted now 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.managed enforces the mode: 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 firing watch dependents 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 status installed
    (dpkg-query -W -f='${db:Status-Status}') instead of the dpkg -s exit
    code — pkg.installed could never reinstall a previously-removed
    conffile-bearing package, and pkg.removed re-applied (and fired watch
    dependents) on every highstate after its own successful removal.
  • pkgrepo.managed verifies the Debian signing key. The key_url key
    now lands persistently at /etc/apt/keyrings/zester-<name>.gpg (was a
    volatile /tmp download) and Check reports drift when it is missing —
    previously a never-imported/deleted key left apt-get update failing
    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,
    and file.recurse compare on-disk owner/group against declared
    user:/group: in Check (declared facets only — undeclared ownership
    never churns). file.recurse Check also flags clean: true extra files
    (previously the clean feature could never fire once the managed set
    converged) and dir_mode drift on every managed dir, and all its
    filesystem walks now go through the injected file provider
    (new FileExec.Walk).
  • service.running/service.dead compare a declared enable: facet in
    Check
    — running-but-disabled (enable: true) and stopped-but-enabled
    (enable: false, resurrects at reboot) were compliant forever.
    service.running with enable: false now actually disables; an Apply
    reached only for enable drift never restarts the running service.
  • user.present converges password and name-based primary group. A
    declared password: hash is compared against the shadow hash (new
    UserExec.PasswordHash); a name-based gid:/primary_group: is compared
    and enforced on existing users via usermod -g — both facets were
    silently unenforced outside user creation.
  • mount.mounted fstab comparison now includes dump/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.present with persist: true verifies the drop-in file entry,
    not just the runtime value — a manual sysctl -w match silently died at
    the next reboot.
  • cron.present entries 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.latest with a symbolic rev: (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
    via git rev-parse --verify <rev>^{commit}; sha prefixes still match
    directly.
  • locale.present verifies the /etc/locale.gen enabling line that
    Apply writes, not just locale -a membership — an out-of-band-generated
    locale was compliant until the next locales package upgrade silently
    dropped it. The facet applies only where /etc/locale.gen exists
    (Debian-family); RHEL/musl systems stay satisfied by locale -a, no
    stray locale.gen is 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's creates probe,
    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/hosts or a user's authorized_keys down to the one
    managed line, or clobber file content that was never captured. Only
    fs.ErrNotExist selects 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_keys restores 0600) and still removes files the same
    instance created. (tolerating an already-externally-removed file). Related honesty fixes: sysctl.present Revert no
    longer writes an EMPTY value into the kernel and persist file;
    mount.mounted Revert no longer claims an unmount it never performed;
    group.present Revert really restores membership (it only restored the
    GID); service.running Revert reports enable-reverts.
  • Watch-forced applies are safe on guarded/absent states. cmd.run's
    creates guard now also gates Apply (a watch trigger used to re-run
    creates-guarded one-shots like initdb); user.absent/group.absent
    Apply no-op cleanly when already absent instead of failing userdel/
    groupdel; service.dead/mount.mounted/sysctl.present/user.present
    Apply can now honestly report changed: false when converged.
    archive.extracted Apply re-evaluates its `i...
Read more