Skip to main content

Butler Releases

Release history and changelog for the Butler Kubernetes-as-a-Service platform.

Component Versions

ComponentLatestTypeReleased
CRD type definitions (the API contract)
0.22.0moduleJun 12, 2026
Core reconciliation controllers
0.28.1containerMay 30, 2026
HTTP/WS API server
0.22.0containerJul 12, 2026
Web console
0.16.1containerJul 10, 2026
CLI tools (butlerctl, butleradm)
0.18.0binaryJun 12, 2026
Management cluster bootstrap controller
0.5.6containerMay 20, 2026
Harvester HCI provider controller
0.2.3containerApr 26, 2026
Nutanix AHV provider controller
0.2.3containerApr 26, 2026
AWS provider controller
0.1.2containerApr 26, 2026
GCP provider controller
--container--
Azure provider controller
--container--
OS image building and hosting service
--container--
Backstage-based developer portal
0.6.2containerJul 17, 2026
Helm charts for deployment
0.14.2helmJul 12, 2026

Kubernetes Compatibility

Butler VersionKubernetes Versions
v0.9.x1.28, 1.29, 1.30, 1.31
v0.8.x1.28, 1.29, 1.30, 1.31
v0.7.x1.28, 1.29, 1.30

Release History

butler-portal0.6.2Jul 17, 2026
  • feat(permissions): adopt @backstage-community/plugin-rbac-backend [BREAKING] by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/40
  • ci: compile-check TypeScript code samples in the permissions docs by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/54
  • docs: RBAC permission framework + updated plugin authoring guide by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/41
  • fix(#52): adopter-plugin reference installs + compiles; drop continue-on-error by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/57
  • docs: adopter-facing note on npm ERESOLVE workaround by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/58
  • chart: pluginsWithPermission guard (fail-loud on drift) + warn-loudly on opt-out by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/42
  • chart(0.6.1): ship #42's guard + #60's fatigue fix to adopters by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/63
  • feat(#59): runtime audit for pluginsWithPermission gaps + addPermissions convention by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/64
butler-portal0.6.0Jul 16, 2026
  • feat(permissions): adopt @backstage-community/plugin-rbac-backend [BREAKING] by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/40
butler-portal0.5.11Jul 15, 2026
  • fix(Dockerfile): bump base to node:22-bookworm-slim for isolated-vm 6.x by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/39
butler-charts0.14.2butler-consoleJul 12, 2026
butler-server0.22.0Jul 12, 2026

Enforces role-based access on the tenant cluster terminal.

Terminal access

The tenant terminal previously admitted any team member, so a viewer could open an interactive shell. Access is now resolved from Team CRDs and gated by role:

  • Write-capable roles (team admin, operator, platform admin) get the interactive shell, unchanged.
  • Viewers connect read-only: they stream output but their input is dropped server-side.
  • Non-members are refused before the connection upgrades.

Authorization boundary

Write-capable roles run the shell under the tenant cluster-admin credential. A scoped operator identity is possible later and would change only the credential selection seam. Because write-capable roles share that credential, each terminal session now logs the Butler user, role, team, cluster, and duration on open and close for attribution.

Deployment

Helm chart butler-console-v0.14.2 advances the pinned server image to 0.22.0.

butler-portal0.5.5Jul 10, 2026

New platform capability

Plugins can now gate their own authorization inside butler-portal without editing the central policy. The new AuthAdjudicatorExtensionPoint routes each authorize() call to a plugin-registered adjudicator by permission-name prefix; unclaimed namespaces fall through to ALLOW so core Backstage plugins (catalog, scaffolder, techdocs, search, kubernetes) keep working unchanged.

Authoring guide: https://github.com/butlerdotdev/butler-portal/blob/v0.5.5/docs/plugin-authoring/authorization.md

  • feat(permissions): namespace-delegated policy so plugins gate their own authz (#33)

Full Changelog: https://github.com/butlerdotdev/butler-portal/compare/v0.5.4...v0.5.5

butler-charts0.14.1butler-consoleJul 10, 2026
butler-console0.16.1Jul 10, 2026

Restores terminal and GitOps write parity for the team admin and operator roles.

Fix

In v0.16.0, users whose highest role was a per-team `admin` or `operator` lost write access to two cluster surfaces:

  • The cluster terminal was read-only. Operators and team admins could open the terminal and see output but could not type.
  • The GitOps Enable and Disable controls were hidden, so operators and team admins could not turn GitOps on or off for a cluster.

Both are now available to team admins and operators, matching the intended role model where `admin` and `operator` are write-capable and `viewer` is read-only. Viewer stays read-only, and platform admin behavior is unchanged.

Deployment

Helm chart `butler-console-v0.14.1` ships this frontend (0.16.1) alongside butler-server 0.21.1. The chart now pins both image tags explicitly.

butler-portal0.5.4Jul 8, 2026
  • fix(Dockerfile): downgrade node base image to node:20-bookworm-slim by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/32
butler-portal0.5.3Jun 22, 2026
  • app: generic sign-in copy for Google card by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/31
butler-portal0.5.2Jun 22, 2026
  • refactor(portal): document upgrade-handler attach window and add guard tests by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/15
  • ci(portal): dedupe push+pull_request runs and bound runner wedges by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/16
  • feat(portal): portal-JWT signing carrier, Stage 2 of the impersonation fix by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/17
  • chore(portal): bump chart 0.2.3 -> 0.3.0 for Stage 2 release by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/18
  • fix(portal): make signing-key mount readable by non-root node UID by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/19
  • feat(portal): runtime per-plugin gates for Butler-Labs-branded plugins by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/20
  • fix(portal): ship packages/app/config.d.ts in backend image (post-PR-#20 boot crashloop hotfix) by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/21
  • feat(portal): chart 0.5.0 dynamic plugins (backend + chart + frontend normalization + installer + boot-test green) by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/22
  • 0.5.1: real frontend dynamic-plugin loading + chart NODE_PATH + CLI wrapper by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/23
  • chart(0.5.3): ConfigMap checksum auto-rollout + postgresql.backup passthrough by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/24
  • chart(0.5.4): allowedSources default includes RHDH marketplace + butlerdotdev by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/25
  • chart(0.5.5): pin installer 0.1.1 (annotation-less OCI pull fix) by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/26
  • chart(0.5.6): pin installer 0.1.2 (auto-detect package.json wrapper dir) by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/27
  • chart(0.5.7): pin installer 0.1.3 (flatten scoped npm names) by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/28
  • app: render Microsoft sign-in card when configured by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/29
  • chart(0.5.8): bump for SignInPage microsoft conditional by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/30
butler-portal0.5.1Jun 14, 2026
  • refactor(portal): document upgrade-handler attach window and add guard tests by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/15
  • ci(portal): dedupe push+pull_request runs and bound runner wedges by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/16
  • feat(portal): portal-JWT signing carrier, Stage 2 of the impersonation fix by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/17
  • chore(portal): bump chart 0.2.3 -> 0.3.0 for Stage 2 release by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/18
  • fix(portal): make signing-key mount readable by non-root node UID by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/19
  • feat(portal): runtime per-plugin gates for Butler-Labs-branded plugins by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/20
  • fix(portal): ship packages/app/config.d.ts in backend image (post-PR-#20 boot crashloop hotfix) by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/21
  • feat(portal): chart 0.5.0 dynamic plugins (backend + chart + frontend normalization + installer + boot-test green) by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/22
  • 0.5.1: real frontend dynamic-plugin loading + chart NODE_PATH + CLI wrapper by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/23
butler-portal0.5.0Jun 14, 2026
  • refactor(portal): document upgrade-handler attach window and add guard tests by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/15
  • ci(portal): dedupe push+pull_request runs and bound runner wedges by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/16
  • feat(portal): portal-JWT signing carrier, Stage 2 of the impersonation fix by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/17
  • chore(portal): bump chart 0.2.3 -> 0.3.0 for Stage 2 release by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/18
  • fix(portal): make signing-key mount readable by non-root node UID by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/19
  • feat(portal): runtime per-plugin gates for Butler-Labs-branded plugins by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/20
  • fix(portal): ship packages/app/config.d.ts in backend image (post-PR-#20 boot crashloop hotfix) by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/21
  • feat(portal): chart 0.5.0 dynamic plugins (backend + chart + frontend normalization + installer + boot-test green) by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/22
butler-portal0.5.0-rc.1preJun 13, 2026
  • refactor(portal): document upgrade-handler attach window and add guard tests by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/15
  • ci(portal): dedupe push+pull_request runs and bound runner wedges by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/16
  • feat(portal): portal-JWT signing carrier, Stage 2 of the impersonation fix by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/17
  • chore(portal): bump chart 0.2.3 -> 0.3.0 for Stage 2 release by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/18
  • fix(portal): make signing-key mount readable by non-root node UID by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/19
  • feat(portal): runtime per-plugin gates for Butler-Labs-branded plugins by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/20
  • fix(portal): ship packages/app/config.d.ts in backend image (post-PR-#20 boot crashloop hotfix) by @atbagan in https://github.com/butlerdotdev/butler-portal/pull/21
butler-cli0.18.0Jun 12, 2026

Changelog

  • feat(cluster): host-usable kubeconfig for local tenants + auto-derive lb-pool (#60)
  • fix(bootstrap): sync embedded CRDs from butler-api and guard against drift (#59)
  • fix(cluster): cross-platform process detach for the local port-forward (#61)
butler-cli0.17.0Jun 12, 2026

Changelog

  • feat: butleradm bootstrap local + TUI wizard for the laptop path (#58)
butler-charts0.22.0butler-crdsJun 12, 2026
butler-api0.22.0Jun 12, 2026

Local provider type for the laptop path

  • New `local` provider for ClusterBootstrap and ProviderConfig: installs Butler onto a kind-based management cluster and provisions tenant workers as containers via the Cluster API Docker provider (CAPD), for running Butler on a laptop with no hypervisor.
  • Adds `ProviderTypeLocal`, an optional `LocalProviderConfig` (`kindNodeImage` override), and `ClusterBootstrap.IsLocal()`.
  • `credentialsRef` is now optional on ProviderConfig (the local provider needs no credentials).

Validation

  • A CEL rule on ProviderConfig requires a non-empty `credentialsRef.name` for every non-local provider and exempts local, so a credential-less non-local config is rejected at admission.

Known follow-up

`ProviderConfig.CredentialsRef` should become `*SecretReference` (tracked in butlerdotdev/butler-api#47) so credential-less local configs round-trip cleanly. Until then a local ProviderConfig shows a cosmetic not-Ready status; provisioning is unaffected.

butler-cli0.16.1Jun 11, 2026

Fix: provider get/list read the correct scope and status paths

A display-path fix, found by exercising `provider get` against a real team-scoped provider.

  • **Scope showed the wrong value.** `provider get` and `provider list -o wide` read `spec.scope` as a string, but the ProviderConfig stores scope as an object (`spec.scope.type`). The string read was always empty, so the `platform` fallback fired and a team-scoped provider displayed as `platform`. They now read `spec.scope.type`, so team-scoped providers show `team` and unscoped providers still show `platform`.
  • **Validation error never displayed.** It was read from a `status.validationError` field that does not exist; a failed validation is recorded as the Ready condition's message, which is now where it is read from.
  • **Last-validation timestamp** was read from `status.lastValidatedAt`; the field is `status.lastValidationTime`.

Verified live on butler-beta with a throwaway team-scoped provider before and after the fix.

butler-server0.21.1Jun 11, 2026
  • feat(auth): log portal proof verification success at Info by @atbagan in https://github.com/butlerdotdev/butler-server/pull/89
butler-cli0.16.0Jun 11, 2026

Team member-role and group access rules (butleradm)

The `butleradm team` group gains four verbs, completing CLI parity with the console for Team access management. Previously the CLI could add and remove members but could not change a member's role or manage group access rules.

| Verb | What it does |

|---|---|

| `update-member-role` | Change an existing member's role |

| `add-group` | Add a group-to-role access rule |

| `update-group-role` | Change a group rule's role |

| `remove-group` | Remove a group rule |

CRD-direct, status-preserving

All four edit `Team.spec.access` directly, mirroring the existing add-member and remove-member: Get the Team, change the targeted entry in the users or groups list, and Update the same object. Because the Update carries the same fetched object, the controller-managed status and the resourceVersion are preserved without a separate merge step.

The user identifier in `spec.access.users` is the `name` field, which holds the email.

Group rules are identified by name plus optional identity provider

A group rule can be scoped to a single IdP with `--identity-provider`, or left unscoped to match a group name from any IdP. When more than one rule shares a name and no `--identity-provider` is given, `update-group-role` and `remove-group` error and ask which one to act on, rather than editing the wrong rule.

The referenced identity provider is not validated to exist when adding a rule (the apiserver does not require it); a convenience check may be added later.

Validation

The full lifecycle was exercised end to end against butler-beta: update-member-role (status conditions and resourceVersion preserved against the real apiserver), add-group, update-group-role, the multi-rule ambiguity guard (errored without mutating), remove-group, and cleanup to pre-state.

butler-charts0.14.0butler-consoleJun 11, 2026
butler-cli0.15.0Jun 10, 2026

P1 #6: IdentityProvider create/update + discovery test (butleradm)

The `butleradm idp` group gains four verbs, closing audit P1 #6 so an operator can stand up SSO from the CLI (previously list/get/delete only).

| Verb | Mechanism | What it does |

|---|---|---|

| `create` | CRD-direct | Create an IdentityProvider from a manifest |

| `update` | CRD-direct | Update the spec, preserving status conditions and resourceVersion |

| `test` | butler-server | Probe an issuer's OIDC discovery before creating a provider |

| `validate` | butler-server | Probe an existing provider's configured issuer |

create and update are CRD-direct

Both read and write the IdentityProvider directly (it is cluster-scoped, so no namespace flag), like the rest of the idp group. `update` does Get-then-merge-spec-then-Update and applies only the spec, so the controller-managed status conditions and the resourceVersion are preserved. A spec edit never clobbers status.

test and validate are read-only probes

Both go through butler-server, which fetches the issuer's `.well-known/openid-configuration` and reports whether discovery succeeds and which endpoints it found. Neither mutates anything, so neither needs a confirmation guard. `test` takes an issuer URL (pre-create); `validate` uses an existing provider's configured issuer.

Secret handling (v1 choice)

The client secret is supplied by reference: the manifest's `spec.oidc.clientSecretRef` points at a Secret created separately, consistent with `provider create`. The console takes the secret inline and creates it server-side; a `--client-secret-from-file` convenience is queued for later. In exchange, the manifest can set any spec field, including `spec.oidc.insecureSkipVerify` and `spec.oidc.googleWorkspace`, which neither client exposed before.

Also fixed

`idp get` and `idp list` read the pre-nesting field paths (`spec.issuerURL`, `spec.clientID`, `spec.scopes`, `spec.claims`), which are always empty on the current CRD because those fields live under `spec.oidc`. They now read `spec.oidc.*`, so the get detail view and the list ISSUER column render correctly. Found by the live lifecycle test.

Validation

The full lifecycle was exercised end to end against butler-beta: test (valid and clean-failure paths), create, get, update (status conditions and resourceVersion preserved against the real apiserver, no optimistic-concurrency conflict), validate, delete, and cleanup to pre-state.

butler-cli0.14.0Jun 10, 2026

P1 #5: ProviderConfig get/update + image/network discovery (butleradm)

The `butleradm provider` group gains four verbs, closing audit P1 #5 so a CLI-only operator can run the full cluster-create workflow without falling back to the console.

| Verb | Mechanism | What it does |

|---|---|---|

| `get` | CRD-direct | Show a ProviderConfig as a detail table, or the full object with `-o json/yaml` |

| `update` | CRD-direct | Update the spec from a YAML file, preserving the existing status and resourceVersion |

| `images` | butler-server | List the images a provider can use, for finding the image ID `cluster create` needs |

| `networks` | butler-server | List the networks a provider can use |

get and update are CRD-direct

Both read and write the ProviderConfig directly, like the rest of the provider group. `update` does Get-then-merge-spec-then-Update and applies only the spec, so the existing validation status and resourceVersion are preserved. A spec edit never clobbers status.

images and networks are policy-filtered

Discovery goes through butler-server so the list is filtered by the same image and network policy the console applies. CLI and console show the same allowed options from one source of truth.

Discovery is supported for the on-premises providers (harvester, nutanix). For cloud providers (aws, azure, gcp) and proxmox the image and network are set directly as fields on the ProviderConfig (for example awsVpcId, gcpImage, azureImageUrn), so those return a clear message naming the provider and pointing at the config fields rather than an error dump.

Validation

Both load-bearing paths are covered by tests: the update merge that preserves status and resourceVersion, and the friendly message for the on-premises-only discovery boundary.

butler-cli0.13.0Jun 10, 2026

Feature 2.3: management cluster GitOps lifecycle (butleradm)

`butleradm` can now manage the management cluster's GitOps lifecycle end to end, closing the management-scope half of the CLI/console capability gap (Feature #4 in the gap audit). This follows Feature 2.2, which did the same for tenant clusters in `butlerctl`.

| Verb | What it does |

|---|---|

| `status` | Whether GitOps is enabled on the management cluster, and the provider, repository, branch, and sync status |

| `discover` | Helm releases and GitOps engine state on the management cluster |

| `preview` | Render the GitOps layout that would be written (non-mutating) |

| `export` | Write the management cluster inventory to a repository (direct push, or `--create-pr`) |

| `enable` | Bootstrap Flux on the management cluster against a repository |

| `disable` | Uninstall Flux from the management cluster |

Full console parity for the management scope. Commands are thin HTTP clients over butler-server's `/api/management/gitops/*` endpoints, consistent with ADR-016.

The disable guard

Management `disable` is the highest-blast-radius operation in the set: it uninstalls the management cluster's own Flux, which manages the platform's components (butler-controller, butler-console) and the image automation that deploys releases. It has no `--yes` shortcut. To proceed, the operator must supply the exact typed phrase `uninstall-management-flux`, by flag or interactive prompt, so a habitual `--yes` cannot fire it. The guard's help and prompt name what it tears down.

Validation

Read verbs (`status`, `discover`, `preview`, `export`) are validated live against the beta management cluster.

`enable` and `disable` are implemented and unit-tested, with live validation deferred by design. Running either against a live management cluster would re-point or tear down the platform's own Flux, so they are covered by unit tests (request shape, error handling, and the confirm guards) rather than a live run. The disable guard's refuse path is mutation-checked.

butler-server0.21.0Jun 10, 2026

# v0.21.0

Stage 1 of the P0 #3 impersonation fix. Adds Ed25519 portal-JWT verification as a new authentication path in `SessionMiddleware`. The legacy `X-Butler-User-Email` header-impersonation behavior is preserved behind `BUTLER_ALLOW_HEADER_IMPERSONATION` (default `true`), so production behavior is unchanged.

**This release changes NOTHING observable.** Console / CLI / portal traffic all flow through the same code paths as v0.20.0. The new portal-JWT branch is dormant unless `BUTLER_PORTAL_PUBKEY` is configured, and the legacy impersonation path is unchanged under the default flag. v0.21.0 adds a verification capability; the cutover that retires header impersonation is Stage 4.

What this release adds

  • `internal/auth/portal_jwt.go`: `PortalJWTVerifier` that validates Ed25519-signed JWT proofs. Verifies signature against a kid-indexed public key map, `iss == "butler-portal"`, `aud == "butler-server"`, `exp` within 60 seconds of `iat` plus 5 seconds of clock-skew slop on the TTL cap, `nbf`, `jti` anti-replay (in-memory cache, lazy sweep, bounded by the 60s TTL), and resolves `sub` against the User CRD via `UserService.GetUserByEmail`.
  • `internal/auth/middleware.go`: new `PortalProofVerifier` interface; new branch in `SessionMiddleware` BEFORE `SessionService.ValidateSession` that routes tokens whose `iss` claim is `butler-portal` to the new verifier; claimed-is-terminal boundary guard that rejects rather than falls through if the verifier reports claimed without a session; legacy header-impersonation sub-block wrapped behind `cfg.AllowHeaderImpersonation`.
  • `internal/config/config.go`: two new env vars: `BUTLER_PORTAL_PUBKEY` (default empty; PEM-encoded Ed25519 public keys for the new verification path) and `BUTLER_ALLOW_HEADER_IMPERSONATION` (default `true`; gates the existing impersonation sub-block).
  • `internal/api/router.go`: constructs `PortalJWTVerifier` only when the pubkey is set; wires both new fields into `SessionMiddlewareConfig`.

Operator action required

**None for v0.21.0.** The default configuration matches pre-Stage-1 behavior:

  • `BUTLER_PORTAL_PUBKEY` defaults to empty. No `PortalJWTVerifier` is constructed; the new branch is structurally bypassed.
  • `BUTLER_ALLOW_HEADER_IMPERSONATION` defaults to `true`. The existing `X-Butler-User-Email` override fires for platform-admin sessions exactly as before.

Operators do not need to set either env var for this release.

Stage roadmap

  • **Stage 1 (this release)**: butler-server gains the verification capability. Additive. Zero behavior change.
  • **Stage 2**: butler-portal swaps its proxy carrier from legacy admin Bearer + `X-Butler-User-Email` to portal-minted Ed25519 proofs. `PortalSigner` mints proofs; `AuthManager` retires. Portal-side change only; this release in butler-server already accepts the proofs once configured.
  • **Stage 3**: butler-portal-live pins the new portal chart + image; new SealedSecret carries the portal's signing key; `BUTLER_PORTAL_PUBKEY` configured on butler-server with the matching public key. End-to-end signed-proof carrier in production.
  • **Stage 4**: `BUTLER_ALLOW_HEADER_IMPERSONATION` flipped to `false`. The legacy header-impersonation sub-block becomes a no-op. After a soak window, the sub-block is removed entirely in a butler-server cleanup PR.

Verification

All three load-bearing invariants in this release are mutation-proven non-vacuous:

  • Iss-routing console-safety: a token with `iss != "butler-portal"` (every console / CLI / legacy admin token) falls through to `ValidateSession` unchanged.
  • Back-compat flag: `AllowHeaderImpersonation=true` preserves the legacy override behavior; `false` skips it.
  • Claimed-is-terminal: when the verifier reports a claimed but invalid proof, the middleware returns 401 instead of falling through to `ValidateSession`.

Rollback

Revert butler-server image and chart to v0.20.0. No data migration, no configuration cleanup required.

butler-cli0.12.0Jun 10, 2026

Feature 2.2: cluster GitOps lifecycle (butlerctl)

`butlerctl` can now manage a tenant cluster's GitOps lifecycle end to end, closing the largest CLI/console capability gap (Feature #4 in the gap audit):

| Verb | What it does |

|---|---|

| `status` | Whether the GitOps engine (Flux) is installed and ready |

| `discover` | Helm releases and GitOps engine state on the cluster |

| `preview` | Render the GitOps layout that would be written (non-mutating) |

| `export` | Write the layout to a repository (direct push, or `--create-pr`) |

| `enable` | Bootstrap Flux on the cluster against a repository |

| `disable` | Remove Flux from the cluster |

For `enable` and `export`, `--repo` is the repository in `owner/repo` form (for example `acme/clusters`), not a URL.

Validated end to end against a real Flux install on a tenant cluster: all verbs pass, the tenant stays healthy through enable and disable.

Delta since v0.11.0

  • feat(cli): cluster gitops lifecycle commands (#52)
butler-server0.20.0Jun 10, 2026

Highlights

  • **`cluster gitops enable` now works for standard GitHub.** Enabling GitOps on a cluster previously returned 500 (`unable to get owner from API`) because the GitHub gitops config URL is the API host (`api.github.com`), which was passed to flux bootstrap as the git `--hostname`. flux then built the git remote against the API host and failed. `api.github.com` is now mapped to flux's default public host, so enable succeeds on both the CLI and the console.
  • GitHub Enterprise and self-hosted GitLab hosts are preserved (their real hostname is still used) via exact-match handling.

Delta since v0.19.0

  • fix(gitops): treat api.github.com as public GitHub for flux bootstrap (#87)
butler-server0.19.0Jun 9, 2026

Highlights

  • **GitOps status false-positive fixed.** A lingering empty `flux-system` namespace no longer reports GitOps as installed/Healthy. Flux is now considered installed only when at least one recognized gotk controller (`source/kustomize/helm/notification-controller`) is present.
  • **Export/preview gate correctness fix (G3).** Export and preview now gate on the Flux engine being **present**, not on controller readiness. The export path reads Helm release Secrets, native resources, and Flux bootstrap CRs from the API server and needs no controller running. As a result:
  • A partial-but-installed Flux is now correctly **exportable** (previously wrongly blocked when fewer than 2 controllers were ready).
  • A `flux-system` holding only non-Flux deployments is now correctly **blocked** (previously wrongly allowed whenever any 2 deployments were ready).

Internals

  • `FluxProvider.CheckInstalled` (CLI status) and `detectFlux` (discover, the export/preview gate, the console banner) now share a single `detectFluxEngine` primitive, so status and discover can no longer report different installed/ready answers for the same cluster. `Installed` and `Ready` are distinct signals (a partial install is Installed, not Ready).

Delta since v0.18.0

  • fix(gitops): unify Flux detection behind one shared primitive (#86)
butler-charts0.16.0steward-etcdJun 9, 2026

v0.9.x Highlights

The v0.9.x release cycle introduced cloud provider support and multi-OS workers:

  • butler-api v0.9.0/v0.9.1: LoadBalancerRequest CRD, cloud-aware bootstrap types, multi-OS worker fields, Azure vmSize/imageURN additions
  • butler-controller v0.9.0: Multi-OS kubeadm bootstrap for non-Talos workers (Rocky, Flatcar, Bottlerocket, Kairos)
  • butler-cli v0.5.0: Cloud bootstrap CLI with credential override flags for GCP, AWS, Azure
  • butler-bootstrap v0.3.0: Cloud bootstrap support, LoadBalancerRequest HA
  • butler-console v0.4.1: Images page and ImageSync management UI
  • butler-server v0.4.1: API dependency bump
  • butler-crds v0.8.0: 17 CRDs including LoadBalancerRequest
  • butler-provider-harvester v0.2.0: butler-api v0.9.0 dependency bump
  • butler-provider-nutanix v0.2.0: butler-api v0.9.0 dependency bump

Cloud Provider Initial Releases

  • butler-provider-aws v0.1.0: AWS EC2 VM provisioning via MachineRequest
  • butler-provider-gcp v0.1.0: GCP Compute Engine VM provisioning via MachineRequest
  • butler-provider-azure v0.1.0: Azure VM provisioning via MachineRequest

Upgrade Guide

From v0.8.x to v0.9.x

  1. Update CRDs first -- helm upgrade butler-crds oci://ghcr.io/butlerdotdev/charts/butler-crds --version 0.8.0 -n butler-system
  2. Upgrade controller -- helm upgrade butler-controller oci://ghcr.io/butlerdotdev/charts/butler-controller --version 0.9.0 -n butler-system
  3. Upgrade console -- helm upgrade butler-console oci://ghcr.io/butlerdotdev/charts/butler-console --version 0.5.0 -n butler-system
  4. Verify -- Check controller logs and TenantCluster status