Resolves N/A
Adds Slack-requested preview environments that can be created and deleted through tuist-ops, deployed by GitHub Actions into the preview Kubernetes cluster, and cleaned up by TTL or idle-time sweeps.
What changed
- Adds
/preview create and /preview delete handling in tuist-ops, persists requests in preview_requests, renders Slack Block Kit cards, and dispatches GitHub Actions with the requested PR or commit.
- Adds the on-demand preview deployment workflow and extends preview sweeping so expired or idle previews uninstall Helm, delete the shared Kura instance, and remove the preview namespace.
- Adds preview Helm and cluster wiring for embedded ClickHouse, embedded object storage, wildcard preview TLS, preview node placement, and local Kind validation with dedicated preview and Kura workers.
- Adds shared preview Kura support: the server can advertise configured Kura endpoints for every account, and the Kura hook keeps tenant matching by default while allowing preview-scoped shared tenants only when explicitly enabled.
- Fixes private Kura instances so the controller skips public Ingress and cert-manager Certificates, and fixes controller image builds for the requested platform.
How it works
- An operator requests a preview from Slack. tuist-ops validates the request, stores it, posts a Slack status card, and dispatches the preview workflow with the slug, PR or commit, TTL, requester, and optional Kura replica count.
- The workflow resolves the target ref, builds the server image, and installs the Tuist chart with the preview overlays. The preview release includes in-cluster Postgres, ClickHouse, and MinIO object storage so it does not point at ClickHouse Cloud or Tigris.
- When Kura is enabled, the workflow creates one preview-scoped
KuraInstance in the kura namespace with a public kura-<slug>.preview.tuist.dev host. The server receives that URL through TUIST_KURA_ENDPOINTS.
- Server cache endpoint discovery returns the configured Kura endpoint for any account when the client requests Kura. This avoids creating per-account
kura_servers rows for previews.
- The Kura Lua hook still authorizes every account and project request through Tuist grants. The only preview-specific relaxation is that
KURA_EXTENSION_TUIST_ALLOW_SHARED_TENANTS=1 lets one preview mesh serve multiple account handles inside the same preview instance.
- The sweep workflow reads namespace labels and annotations, deletes previews whose TTL expired or whose last-active timestamp is too old, and removes the matching shared Kura instance.
Why
Previews are isolated at the instance level, so a single Kura mesh per preview is simpler than provisioning per-account Kura nodes inside that preview. It gives every account in the preview the same cache endpoint while preserving account and project authorization in the Kura hook. Embedding ClickHouse and object storage makes previews closer to staging, canary, and production behavior without sharing managed production-like data services.
User and developer impact
- Operators can request, inspect, and delete preview environments from Slack.
- Preview environments carry their own analytics and object-storage dependencies.
- Kura in previews is instance-scoped and shared across accounts, not represented as account-specific managed Kura servers.
- Expiration and idle cleanup happen from GitHub Actions using Kubernetes labels and annotations.
How to test locally
cd tuist-ops && mix format && mix test test/tuist_ops_web/controllers/slack_controller_test.exs test/jit/changesets_test.exs
ruby -e 'require "yaml"; ARGV.each { |f| YAML.load_file(f); puts "OK #{f}" }' .github/workflows/preview-ondemand-deploy.yml .github/workflows/preview-sweep.yml infra/helm/tuist/values-preview-kura.yaml infra/k8s/kind-preview.yaml
bash -n infra/mise/tasks/helm/preview-up.sh && bash -n infra/mise/tasks/helm/preview-down.sh
helm lint infra/helm/tuist -f infra/helm/tuist/values-preview.yaml -f infra/helm/tuist/values-preview-kura.yaml --set server.image.tag=latest --set server.ingress.enabled=true --set server.ingress.host=demo.preview.tuist.dev --set server.appUrl=https://demo.preview.tuist.dev --set kuraController.image.tag=latest --set kuraRuntime.image.tag=latest
helm template ondemand-demo infra/helm/tuist -f infra/helm/tuist/values-preview.yaml -f infra/helm/tuist/values-preview-kura.yaml --set server.image.tag=latest --set server.ingress.enabled=true --set server.ingress.host=demo.preview.tuist.dev --set server.appUrl=https://demo.preview.tuist.dev --set kuraController.image.tag=latest --set kuraRuntime.image.tag=latest >/tmp/tuist-preview-ondemand-rendered.yaml
helm dependency build infra/helm/tuist
helm template preview-test infra/helm/tuist -f infra/helm/tuist/values-preview.yaml -f infra/helm/tuist/values-preview-kura.yaml --set server.image.tag=test --set server.ingress.enabled=true --set server.ingress.host=preview-test.preview.tuist.dev --set server.appUrl=https://preview-test.preview.tuist.dev --set kuraController.image.tag=test --set kuraRuntime.image.tag=test --set "server.kuraEndpointUrls[0]=https://kura-preview-test.preview.tuist.dev" >/tmp/tuist-preview-kura-render.yaml
ruby -e 'content = File.read(ARGV.fetch(0)); %w[TUIST_KURA_ENDPOINTS TUIST_KURA_RUNTIME_IMAGE_TAG letsencrypt-cloudflare preview-test-tuist-clickhouse preview-test-tuist-object-storage].each { |needle| abort("missing #{needle}") unless content.include?(needle) }; abort("unexpected TUIST_KURA_AVAILABLE_REGIONS") if content.include?("TUIST_KURA_AVAILABLE_REGIONS"); puts "render assertions passed"' /tmp/tuist-preview-kura-render.yaml
cd infra/kura-controller && mise exec go -- go test ./controllers
cd kura && mise exec -- cargo fmt --check
cd kura && mise exec -- cargo test tuist_hook
cd server && MIX_ENV=test mise exec -- mix ecto.reset
cd server && mise exec -- mix test test/tuist/environment_test.exs:290 test/tuist/environment_test.exs:297 test/tuist/accounts_test.exs:4220 test/tuist/accounts_test.exs:4233
git diff --check
git diff --cached --check
rg -n "^(<<<<<<<|=======$|>>>>>>> )" .
- Local Kind validation created a five-node
tuist-preview cluster. The full server rollout reached dependency scheduling for ClickHouse, object storage, Postgres, cache, and server on the preview worker, but server readiness needs a valid TUIST_LICENSE_KEY or OP_SERVICE_ACCOUNT_TOKEN, which was not available locally. A controller-only Kura validation created a private two-replica KuraInstance; both runtime pods reached Ready on separate role=kura Kind workers.