improvement(helm): hygiene pass — CI gating, strict values schema, ESO v1 default (#5939)

* improvement(helm): hygiene pass — CI gating, strict values schema, ESO v1 default, ci values

Closes the gaps from a best-practices audit of the chart (template-level
conformance was already clean: full label set, 82 unit tests, kubeconform-
valid renders):

- new Helm Chart workflow gates every chart change: helm lint, the 82
  helm-unittest cases, kubeconform validation of default + all-components
  renders (k8s 1.29 strict, CRD catalog), and a render of all 10 example
  values files
- helm/sim/ci/ values files (chart-testing convention) so the chart lints
  and templates cleanly out of the box with dummy secrets
- values.schema.json declares all 30 top-level keys (16 were invisible) and
  sets root additionalProperties: false, so top-level typos fail fast
- externalSecrets.apiVersion defaults to v1: current ESO releases removed
  the v1beta1 compatibility path in 2026, so the old default produced
  rejected manifests on new installs; NOTES/values comments updated
- wait-for-postgres init container gets requests/limits (the only container
  in the chart without them; broke ResourceQuota'd namespaces)
- drop the telemetry Prometheus scrape config for app/realtime — neither
  exposes /metrics, so it was dead config that also rendered a realtime
  target with realtime disabled
- chart 1.2.0 with README upgrade notes

* fix(helm): version-agnostic schema-error assertions in the secret-length suite

Helm v4 phrases schema rejections as 'minLength: got N, want 32' while
v3 says 'String length must be greater than or equal to 32'. The suite
grepped for 'minLength' only, so it passed on local helm v4 and failed on
CI's v3.16.4 — the enforcement itself works on both. Patterns now assert
the key name plus either wording. Caught by the new Helm Chart workflow
on its very first run.

* improvement(helm): kind install test + chart version-bump gate in CI

Benchmarked against the flagship OSS charts (ingress-nginx, argo-cd,
kube-prometheus-stack, grafana, bitnami, cert-manager): sim already exceeds
most of them on validation rigor (strict schema — 4 of 6 ship none; 82 unit
tests vs argo-cd's zero; kubeconform manifest validation none of them run),
but every top community chart repo actually installs the chart on a kind
cluster in chart CI — the one majority practice we lacked. Adds:

- install job: kind cluster, helm install with ci/default + a new
  small-footprint ci/kind-values.yaml overlay (default app requests of 4Gi
  can't schedule on a CI node), --wait, then the chart's helm test hook,
  with pod/event/log diagnostics on failure
- version-bump job (PR-only): fails when helm/sim/** changes without a
  Chart.yaml version increment — argo/kps/grafana all enforce this

* fix(helm): declare naming overrides in the strict schema; SHA-pin CI actions

- nameOverride/fullnameOverride are consumed by sim.name/sim.fullname but
  were never in values.yaml, so root additionalProperties: false rejected
  Helm's standard naming overrides — both now declared (a helper-wide sweep
  confirmed they were the only template-read keys missing), with a smoke
  regression test that installs under the strict schema and asserts the
  override lands in resource names
- CI supply-chain hardening: checkout/setup-helm/kind-action pinned to full
  commit SHAs (tag comments retained) and the kubeconform archive verified
  against its published sha256

* fix(helm): immutable unittest runner and fail-closed version gate in CI

- helm-unittest runs via the project's official docker image pinned by
  immutable sha256 digest instead of a plugin install from a mutable git tag
- the version-bump gate fetches the full base ref (a --depth=1 fetch could
  leave no merge base), computes the merge-base and diff outside the if so
  any git failure fails the job instead of falling into the skip branch

* fix(ci): run the helm-unittest container as the runner UID

The digest-pinned image runs as a non-root user that cannot write into the
runner-owned bind mount (it creates tests/__snapshot__, absent from the
checkout since empty dirs aren't tracked). Standard bind-mount pattern:
--user "$(id -u):$(id -g)" with HOME=/tmp for helm's cache.

* fix(helm): appVersion points at a real GHCR tag (v0.7.44)

The kind install job caught this on its first full run: the default image
tag (Chart.AppVersion 0.6.73) returns 404 on GHCR for all three images —
the registry's tags are v-prefixed — so an unpinned default install could
never pull. Updated appVersion to v0.7.44 (verified 200 for simstudio,
realtime, and migrations manifests), stale values comment refreshed, and
an upgrade note added. The CI kind values deliberately stay tag-free so
the job keeps exercising the true default path.
This commit is contained in:
Waleed
2026-07-24 14:25:46 -07:00
committed by GitHub
parent f0f98708fc
commit 90f6708270
17 changed files with 395 additions and 27 deletions
+157
View File
@@ -0,0 +1,157 @@
name: Helm Chart
on:
push:
branches: [main, staging, dev]
paths:
- 'helm/sim/**'
- '.github/workflows/helm.yml'
pull_request:
branches: [main, staging, dev]
paths:
- 'helm/sim/**'
- '.github/workflows/helm.yml'
concurrency:
group: helm-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
chart:
name: Lint, test, and validate chart
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Set up Helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
with:
version: v3.16.4
- name: Helm lint
run: helm lint helm/sim --values helm/sim/ci/default-values.yaml
- name: Helm unit tests
run: |
# Official helm-unittest image, pinned by immutable digest (tag 3.17.3-0.8.2).
# Run as the runner's UID so the container can write into the bind
# mount (it creates tests/__snapshot__), with a writable HOME for helm.
docker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp \
-v "$PWD/helm/sim:/apps" \
helmunittest/helm-unittest@sha256:b653db7d5665bc6cec677b15c5eaa1c0377c0de8ac4eb1df58b924478baa21e1 .
- name: Install kubeconform
run: |
curl -sSL -o /tmp/kubeconform.tar.gz \
https://github.com/yannh/kubeconform/releases/download/v0.6.7/kubeconform-linux-amd64.tar.gz
echo "95f14e87aa28c09d5941f11bd024c1d02fdc0303ccaa23f61cef67bc92619d73 /tmp/kubeconform.tar.gz" | sha256sum -c -
tar -xzf /tmp/kubeconform.tar.gz -C /tmp kubeconform
- name: Render and validate manifests (default configuration)
run: |
helm template sim helm/sim --namespace sim \
--values helm/sim/ci/default-values.yaml \
| /tmp/kubeconform -strict -summary \
-kubernetes-version 1.29.0 \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json'
- name: Render and validate manifests (all components enabled)
run: |
helm template sim helm/sim --namespace sim \
--values helm/sim/ci/full-values.yaml \
| /tmp/kubeconform -strict -summary \
-kubernetes-version 1.29.0 \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json'
- name: Render every example values file
run: |
set -euo pipefail
for f in helm/sim/examples/values-*.yaml; do
echo "--- $f"
# Examples intentionally omit secrets (their headers document the
# required --set flags), so supply the CI dummies alongside each.
helm template sim helm/sim --namespace sim \
--values "$f" \
--values helm/sim/ci/default-values.yaml \
--set copilot.postgresql.auth.password=ci-dummy-password \
--set copilot.server.env.AGENT_API_DB_ENCRYPTION_KEY=cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici \
--set copilot.server.env.INTERNAL_API_SECRET=cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici \
--set copilot.server.env.LICENSE_KEY=ci-dummy-license \
--set copilot.server.env.SIM_BASE_URL=https://ci.example.com \
--set copilot.server.env.SIM_AGENT_API_KEY=ci-dummy-agent-key \
--set copilot.server.env.REDIS_URL=redis://ci-redis:6379 \
--set copilot.server.env.OPENAI_API_KEY_1=ci-dummy-openai-key \
--set externalDatabase.password=ci-dummy-password > /dev/null
done
version-bump:
name: Chart version bumped
if: github.event_name == 'pull_request'
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
timeout-minutes: 5
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
- name: Require a Chart.yaml version bump when chart content changes
run: |
set -euo pipefail
base="origin/${{ github.base_ref }}"
git fetch origin "${{ github.base_ref }}"
merge_base=$(git merge-base "$base" HEAD)
changed=$(git diff --name-only "$merge_base" HEAD)
if echo "$changed" | grep -q '^helm/sim/'; then
base_version=$(git show "$merge_base:helm/sim/Chart.yaml" | awk '/^version:/ {print $2}')
head_version=$(awk '/^version:/ {print $2}' helm/sim/Chart.yaml)
echo "base=$base_version head=$head_version"
if [ "$base_version" = "$head_version" ]; then
echo "::error::helm/sim/** changed but Chart.yaml version did not (still $head_version). Bump it per SemVer."
exit 1
fi
else
echo "No chart changes; skipping."
fi
install:
name: Install on kind and run helm test
needs: chart
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
timeout-minutes: 25
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Set up Helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
with:
version: v3.16.4
- name: Create kind cluster
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1
with:
version: v0.24.0
- name: Install chart
run: |
helm install sim helm/sim \
--namespace sim --create-namespace \
--values helm/sim/ci/default-values.yaml \
--values helm/sim/ci/kind-values.yaml \
--wait --timeout 15m
- name: Diagnostics on failure
if: failure()
run: |
kubectl -n sim get pods -o wide || true
kubectl -n sim get events --sort-by=.lastTimestamp | tail -40 || true
kubectl -n sim describe pods | tail -100 || true
kubectl -n sim logs deploy/sim-app -c migrations --tail=50 || true
kubectl -n sim logs deploy/sim-app --tail=80 || true
- name: Run helm test
run: helm test sim --namespace sim --timeout 5m
+2 -2
View File
@@ -2,8 +2,8 @@ apiVersion: v2
name: sim
description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents
type: application
version: 1.1.0
appVersion: "0.6.73"
version: 1.2.0
appVersion: "v0.7.44"
kubeVersion: ">=1.25.0-0"
home: https://sim.ai
icon: https://raw.githubusercontent.com/simstudioai/sim/main/apps/sim/public/logo/primary/primary.svg
+7
View File
@@ -467,6 +467,13 @@ kubectl --namespace sim logs deploy/sim-app -c migrations
---
## Upgrading to 1.2.0
* `appVersion` (the default image tag when `image.tag` is unset) is now `v0.7.44` — the previous `0.6.73` referenced a tag that does not exist on GHCR, so an unpinned default install could not pull images. Production installs should still pin `image.tag` explicitly.
* `externalSecrets.apiVersion` now defaults to `"v1"` — current External Secrets Operator releases no longer serve `v1beta1` (removed upstream in 2026). Set `externalSecrets.apiVersion: "v1beta1"` only if you still run ESO < 0.17.
* `values.schema.json` now declares every top-level key and rejects unknown top-level keys, so a typo like `networkPolciy:` fails fast at install time instead of being silently ignored. If an upgrade suddenly fails schema validation, check your values file for stray top-level keys.
* The opt-in telemetry collector no longer ships a Prometheus scrape config for the app/realtime services (they expose no `/metrics` endpoint); OTLP ingestion is unchanged.
## Upgrading to 1.1.0
No action is required for working configurations. Notes:
+12
View File
@@ -0,0 +1,12 @@
# CI-only values: the minimum required secrets so `helm lint`/`helm template`
# render the DEFAULT configuration. Dummy values — never use in a deployment.
app:
env:
BETTER_AUTH_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici"
ENCRYPTION_KEY: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici"
INTERNAL_API_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici"
CRON_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici"
postgresql:
auth:
password: "ci-dummy-password"
+72
View File
@@ -0,0 +1,72 @@
# CI-only values: every optional component enabled so the full template surface
# renders and validates. Dummy values — never use in a deployment.
app:
env:
BETTER_AUTH_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici"
ENCRYPTION_KEY: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici"
INTERNAL_API_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici"
CRON_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici"
API_ENCRYPTION_KEY: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici"
postgresql:
auth:
password: "ci-dummy-password"
tls:
enabled: true
certManager:
enabled: true
ingress:
enabled: true
app:
host: ci.example.com
paths: [{ path: /, pathType: Prefix }]
realtime:
host: ci-ws.example.com
paths: [{ path: /, pathType: Prefix }]
tls:
enabled: true
networkPolicy:
enabled: true
autoscaling:
enabled: true
monitoring:
serviceMonitor:
enabled: true
telemetry:
enabled: true
jaeger:
enabled: true
prometheus:
enabled: true
otlp:
enabled: true
sharedStorage:
enabled: true
ollama:
enabled: true
pii:
enabled: true
copilot:
enabled: true
postgresql:
auth:
password: "ci-dummy-password"
server:
env:
AGENT_API_DB_ENCRYPTION_KEY: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici"
INTERNAL_API_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici"
LICENSE_KEY: "ci-dummy-license"
SIM_BASE_URL: "https://ci.example.com"
SIM_AGENT_API_KEY: "ci-dummy-agent-key"
REDIS_URL: "redis://ci-redis:6379"
OPENAI_API_KEY_1: "ci-dummy-openai-key"
+39
View File
@@ -0,0 +1,39 @@
# CI-only overlay for the kind install test: shrink resource requests so the
# default configuration schedules on a small CI runner. Layered on top of
# ci/default-values.yaml. Dummy sizing — never use in a deployment.
app:
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: 1000m
memory: 2Gi
realtime:
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
migrations:
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 1Gi
postgresql:
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 1Gi
persistence:
size: 2Gi
+3 -3
View File
@@ -86,9 +86,9 @@ Your release is named {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
5. Upgrade notes (read before upgrading from a chart version released before this one):
* externalSecrets.apiVersion default is "v1beta1" (was "v1"). v1beta1 is
supported by every ESO release from v0.7+ through current. If you're on
ESO v0.17+ and want the graduated v1 API, set externalSecrets.apiVersion: "v1".
* externalSecrets.apiVersion defaults to "v1". Current ESO releases no
longer serve v1beta1 (the compatibility path was removed in 2026) — set
externalSecrets.apiVersion: "v1beta1" only if you still run ESO < 0.17.
* networkPolicy.egress remains a list of custom egress rules (unchanged).
Cloud-metadata CIDR blocking is now configured via networkPolicy.egressExceptCidrs
(defaults to AWS/GCP/Azure IMDS + ECS task metadata).
+1 -1
View File
@@ -9,7 +9,7 @@
# - a string: treated as the remoteRef.key (legacy form)
# - a map: passed through as the remoteRef block (e.g. {key, property,
# version, decodingStrategy, conversionStrategy, metadataPolicy})
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1beta1" }}
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1" }}
kind: ExternalSecret
metadata:
name: {{ include "sim.fullname" . }}-app-secrets
@@ -6,7 +6,7 @@
# template rendering if a required key (or any non-empty copilot.server.env key) is
# missing a matching remoteRefs.copilot entry, so a misconfiguration surfaces at
# `helm template` time instead of a container starting with an empty value.
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1beta1" }}
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1" }}
kind: ExternalSecret
metadata:
name: {{ include "sim.copilot.envSecretName" . }}
@@ -1,6 +1,6 @@
{{- if and .Values.externalSecrets.enabled .Values.externalDatabase.enabled .Values.externalSecrets.remoteRefs.externalDatabase.password }}
# ExternalSecret for external database password (syncs from external secret managers)
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1beta1" }}
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1" }}
kind: ExternalSecret
metadata:
name: {{ include "sim.fullname" . }}-external-db-secret
@@ -1,6 +1,6 @@
{{- if and .Values.externalSecrets.enabled .Values.postgresql.enabled .Values.externalSecrets.remoteRefs.postgresql.password }}
# ExternalSecret for PostgreSQL password (syncs from external secret managers)
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1beta1" }}
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1" }}
kind: ExternalSecret
metadata:
name: {{ include "sim.fullname" . }}-postgresql-secret
@@ -45,6 +45,13 @@ spec:
sleep 2
done
echo "Copilot PostgreSQL is ready!"
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
envFrom:
- secretRef:
name: {{ include "sim.fullname" . }}-copilot-postgresql-secret
+1 -11
View File
@@ -18,16 +18,6 @@ data:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
scrape_configs:
- job_name: 'sim-app'
static_configs:
- targets: ['{{ include "sim.fullname" . }}-app:{{ .Values.app.service.port }}']
- job_name: 'sim-realtime'
static_configs:
- targets: ['{{ include "sim.fullname" . }}-realtime:{{ .Values.realtime.service.port }}']
processors:
batch:
timeout: 1s
@@ -83,7 +73,7 @@ data:
- otlp
{{- end }}
metrics:
receivers: [otlp, prometheus]
receivers: [otlp]
processors: [memory_limiter, batch]
exporters:
- logging
@@ -13,7 +13,7 @@ tests:
postgresql.auth.password: xxxxxxxx
asserts:
- failedTemplate:
errorPattern: "BETTER_AUTH_SECRET.*minLength"
errorPattern: "BETTER_AUTH_SECRET.*(minLength|greater than or equal to 32)"
- it: rejects an ENCRYPTION_KEY shorter than 32 characters
set:
@@ -24,7 +24,7 @@ tests:
postgresql.auth.password: xxxxxxxx
asserts:
- failedTemplate:
errorPattern: "ENCRYPTION_KEY.*minLength"
errorPattern: "ENCRYPTION_KEY.*(minLength|greater than or equal to 32)"
- it: rejects a postgresql.auth.password shorter than 8 characters
set:
@@ -35,7 +35,7 @@ tests:
postgresql.auth.password: short
asserts:
- failedTemplate:
errorPattern: "password.*minLength"
errorPattern: "password.*(minLength|greater than or equal to 8)"
- it: accepts an empty BETTER_AUTH_SECRET (deferred to existingSecret/ESO)
templates:
+10
View File
@@ -33,3 +33,13 @@ tests:
asserts:
- isKind: { of: Secret }
- equal: { path: metadata.name, value: t-sim-app-secrets }
- it: accepts the standard Helm naming overrides under the strict schema
set:
nameOverride: acme
fullnameOverride: acme-sim
asserts:
- matchRegex:
path: metadata.name
pattern: "^acme-sim"
template: deployment-app.yaml
+73
View File
@@ -1190,8 +1190,81 @@
}
}
}
},
"nameOverride": {
"type": "string",
"description": "Override the chart name used in resource names"
},
"fullnameOverride": {
"type": "string",
"description": "Override the fully qualified release name used in resource names"
},
"affinity": {
"type": "object",
"description": "Pod affinity/anti-affinity rules applied to app and realtime pods"
},
"branding": {
"type": "object",
"description": "Whitelabel branding configuration (logos, names, colors)"
},
"certManager": {
"type": "object",
"description": "Chart-managed cert-manager issuers (self-signed bootstrap + CA) for postgresql.tls"
},
"cronjobs": {
"type": "object",
"description": "Scheduled jobs (schedule execution, polling, reconciliation)"
},
"extraEnvVars": {
"type": "array",
"description": "Additional environment variables added to app and realtime containers"
},
"extraVolumeMounts": {
"type": "array",
"description": "Additional volume mounts added to app and realtime containers"
},
"extraVolumes": {
"type": "array",
"description": "Additional volumes added to app and realtime pods"
},
"ingressInternal": {
"type": "object",
"description": "Secondary internal ingress configuration"
},
"migrations": {
"type": "object",
"description": "Database migrations init-container configuration"
},
"monitoring": {
"type": "object",
"description": "Prometheus Operator ServiceMonitor configuration"
},
"networkPolicy": {
"type": "object",
"description": "NetworkPolicy toggles and custom ingress/egress rules"
},
"podAnnotations": {
"type": "object",
"description": "Annotations added to app and realtime pods"
},
"podDisruptionBudget": {
"type": "object",
"description": "PodDisruptionBudget configuration for app and realtime"
},
"podLabels": {
"type": "object",
"description": "Labels added to app and realtime pods"
},
"serviceAccount": {
"type": "object",
"description": "ServiceAccount creation, name, and annotations (e.g. Workload Identity)"
},
"tolerations": {
"type": "array",
"description": "Tolerations applied to app and realtime pods"
}
},
"additionalProperties": false,
"allOf": [
{
"if": {
+5 -4
View File
@@ -20,7 +20,7 @@ app:
# Image configuration
image:
repository: simstudioai/simstudio
# tag defaults to Chart.AppVersion. Override with a release tag (e.g. "0.6.73") or pin via image.digest.
# tag defaults to Chart.AppVersion. Override with a release tag (e.g. "v0.7.44") or pin via image.digest.
tag: ""
# Optional image digest pin: "sha256:..." — when set, overrides tag.
digest: ""
@@ -1729,9 +1729,10 @@ externalSecrets:
enabled: false
# ESO API version. Default "v1beta1" — supported by every ESO release from
# v0.7+ (mid-2023) through current. Set to "v1" only when targeting ESO
# v0.17+ clusters where the v1 API has graduated.
apiVersion: "v1beta1"
# Current ESO releases serve only external-secrets.io/v1 (the v1beta1
# compatibility path was removed in 2026). Set to "v1beta1" only for
# clusters still running ESO < 0.17.
apiVersion: "v1"
# How often to sync secrets from the external store
refreshInterval: "1h"