Hive Hive
Sign in

feat: add Slack-requested preview environments

GitHub issue · Closed

Metadata
Source
tuist/tuist #11348
Updated
Jun 24, 2026
Domains
Compute Kura
Details

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.
Comments
P
pepicrft Jun 19, 2026

Honest answer: I haven’t run a Slack preview end-to-end against the real preview cluster, and I should have flagged that earlier.

The chicken-and-egg is the immediate blocker — gh workflow run requires the workflow file to exist on main first (GitHub looks up the workflow by filename in the default branch’s registry, regardless of --ref). So I can’t dispatch preview-deploy.yml against this branch until either (a) the workflow file lands on main ahead of the rest of the PR, or (b) the PR merges. Same wall hits the Slack /preview path because tuist-ops uses the same dispatch API. The local kind validation (mise run helm:preview-up) needs an OP_SERVICE_ACCOUNT_TOKEN or TUIST_LICENSE_KEY that I don’t have access to.

What’s actually been validated so far is static:

  • helm template renders for both profiles + helm lint is clean
  • helm template --show-only templates/kura-controller.yaml renders the kura-platform install path
  • The chart helper resolves KURA_CONTROL_PLANE_CLIENT_ID/SECRET correctly for managed (kuraController.enabled=true) and preview (server.kuraIntrospection.useSharedSecret=true) layouts
  • actionlint is clean across the consolidated workflow modulo the self-hosted runner labels
  • kura-controller Go build + go test ./controllers passes, including the flip-to-private Certificate cleanup test
  • 161 tuist-ops tests pass, including the delete-then-recreate-same-slug path through Previews.create / Previews.delete
  • The server Accounts.get_cache_endpoints_for_handle/2 tests pass for the new (no shared-mesh) path

What I have not validated behaviorally:

  • That the preview-deploy.yml resolver maps the Slack dispatch shape correctly under real conditions
  • That the seed kubectl exec command actually runs to completion against the server pod and the Tuist.Accounts / FunWithFlags API I called (I inferred the signatures from grepping the server)
  • That a cache read/write actually flows through the preview Kura with strict tenant matching
  • That delete + recreate for the same slug works end-to-end through the consolidated workflow

To actually verify, I’d need one of:

  1. Cherry the consolidated workflow + chart bits to main as a separate small PR. ~5 additive files, no-op until called. Then I can gh workflow run preview-deploy.yml --ref chore/i-d-like-to-design with a slug and walk through the full create → cache read/write → delete → recreate flow. This is the path I’d prefer.
  2. Hand me an OP_SERVICE_ACCOUNT_TOKEN or TUIST_LICENSE_KEY (or run mise run helm:preview-up yourself) so the local kind validation can run.
  3. Merge first, validate after — which I know inverts the order you’d want.

Option 1 is what I’d recommend; want me to prep that cherry-PR?

GA
github-actions[bot] Jun 19, 2026

⚪ Preview torn down (2026-06-23 09:59 UTC). Re-add the preview label to bring it back.