From b146084c2fe1052cb8b16f7269ad32f6895532d8 Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Wed, 4 Nov 2026 20:43:51 +0100 Subject: [PATCH 1/8] fix cluster tools --- tools/just/cluster.just | 172 +++++++++++++++++++++++++--------------- 1 file changed, 106 insertions(+), 66 deletions(-) diff --git a/tools/just/cluster.just b/tools/just/cluster.just index fe082d98..8bd51935 100644 --- a/tools/just/cluster.just +++ b/tools/just/cluster.just @@ -4,7 +4,6 @@ set shell := ["bash", "-cue"] root_dir := `git rev-parse --show-toplevel` # Names the chart gives a release called `hackagon` (see helm-chart/templates). -namespace := "hackagon" backend_deploy := "hackagon-backend" backend_config := "hackagon-backend-config" backend_db_secret := "hackagon-backend-db" @@ -14,13 +13,23 @@ postgres_svc := "hackagon-postgresql" app_db := "hackagon" app_db_owner := "hackagon" -# Refuse any cluster whose ingress does not serve this domain, so a recipe -# pointed at the prod context by mistake stops before touching anything. -dev_domain := "hackagon-dev.dscompute.ch" - backup_dir := env("HOME") + "/hackagon-backups" local_pg_port := "15432" +# Sourced by every recipe: `resolve_instance ` sets the namespace and the +# host its ingress must serve. Only these two names exist, so a recipe can never +# be pointed at prod, and the host check catches a context that is not the one +# holding them. +instances := ''' +resolve_instance() { + case "$1" in + dev) ns=hackagon-dev; host=hackagon-dev.sck-sit-dev.dscompute.ch ;; + staging) ns=hackagon-staging; host=hackagon-staging.sck-sit-dev.dscompute.ch ;; + *) echo "✗ Unknown instance '$1': expected dev or staging." >&2; exit 1 ;; + esac +} +''' + # Runs inside the postgres pod: finds the superuser password the bitnami image # was started with (env var or mounted file) so it never leaves the pod. pg_as_superuser := ''' @@ -45,53 +54,59 @@ help: reset="\033[0m" echo "" - echo -e " ${bold}Dev Cluster Commands${reset}" - echo -e " ${dim}Act on the deployed dev instance. Every recipe takes the kube context${reset}" - echo -e " ${dim}explicitly and refuses a cluster not serving {{dev_domain}}.${reset}" + echo -e " ${bold}Cluster Commands${reset}" + echo -e " ${dim}Act on a deployed instance — dev or staging, never prod. Every recipe${reset}" + echo -e " ${dim}takes the instance and the kube context explicitly, and refuses a${reset}" + echo -e " ${dim}namespace whose ingress does not serve that instance's host.${reset}" echo "" - echo -e " ${cyan}just cluster::backup${reset} " - echo -e " ${dim}pg_dumpall the dev postgres to {{backup_dir}}${reset}" + echo -e " ${cyan}just cluster::backup${reset} " + echo -e " ${dim}pg_dumpall the instance's postgres to {{backup_dir}}${reset}" echo "" - echo -e " ${cyan}just cluster::wipe${reset} " + echo -e " ${cyan}just cluster::wipe${reset} " echo -e " ${dim}Back up, then drop and recreate the app database. Keycloak is untouched.${reset}" - echo -e " ${dim}The backend recreates the schema on startup, so dev comes back empty.${reset}" + echo -e " ${dim}The backend recreates the schema on startup, so the instance comes back empty.${reset}" echo "" - echo -e " ${cyan}just cluster::reseed${reset} " - echo -e " ${dim}Back up, wipe, then run cmd/seed (backend code must match origin/main).${reset}" + echo -e " ${cyan}just cluster::reseed${reset} " + echo -e " ${dim}Back up, wipe, then run cmd/seed. components/backend must match what the${reset}" + echo -e " ${dim}instance serves: origin/main for dev, the newest origin/release/* for staging.${reset}" echo "" - echo -e " ${dim}e.g. just cluster::wipe sck-sit-dev${reset}" + echo -e " ${dim}e.g. just cluster::reseed staging sck-sit-dev${reset}" echo "" -# pg_dumpall the dev postgres (app and keycloak databases) to ~/hackagon-backups. +# pg_dumpall the instance's postgres (app and keycloak databases) to ~/hackagon-backups. [group('cluster')] -backup context: +backup instance context: #!/usr/bin/env bash set -euo pipefail - just -f "{{source_file()}}" _guard "{{context}}" + {{instances}} + resolve_instance "{{instance}}" + just -f "{{source_file()}}" _guard "{{instance}}" "{{context}}" mkdir -p "{{backup_dir}}" - out="{{backup_dir}}/{{context}}-$(date +%Y%m%d-%H%M%S).sql" + out="{{backup_dir}}/{{context}}-{{instance}}-$(date +%Y%m%d-%H%M%S).sql" discard_partial() { rm -f "$out.partial"; } trap discard_partial EXIT echo "==> Backing up to $out" - kubectl --context "{{context}}" -n "{{namespace}}" exec "sts/{{postgres_sts}}" -- \ + kubectl --context "{{context}}" -n "$ns" exec "sts/{{postgres_sts}}" -- \ sh -c '{{pg_as_superuser}}' sh pg_dumpall -h 127.0.0.1 -U postgres > "$out.partial" mv "$out.partial" "$out" echo "✓ $(du -h "$out" | cut -f1) written" -# Back up, then drop and recreate the app database; dev restarts empty. +# Back up, then drop and recreate the app database; the instance restarts empty. [group('cluster')] -wipe context: +wipe instance context: #!/usr/bin/env bash set -euo pipefail - just -f "{{source_file()}}" _confirm "{{context}}" "WIPE the app database" - just -f "{{source_file()}}" backup "{{context}}" + {{instances}} + resolve_instance "{{instance}}" + just -f "{{source_file()}}" _confirm "{{instance}}" "{{context}}" "WIPE the app database" + just -f "{{source_file()}}" backup "{{instance}}" "{{context}}" - kc=(kubectl --context "{{context}}" -n "{{namespace}}") + kc=(kubectl --context "{{context}}" -n "$ns") # A backend found at 0 (e.g. left there by an interrupted run) comes back - # at 1: both recipes promise a running dev, and wipe needs it to recreate - # the schema. + # at 1: both recipes promise a running instance, and wipe needs it to + # recreate the schema. replicas="$("${kc[@]}" get "deploy/{{backend_deploy}}" -o jsonpath='{.spec.replicas}')" [ "${replicas:-0}" -gt 0 ] || replicas=1 @@ -103,20 +118,22 @@ wipe context: # however this ends, including when stopping it or the wipe fails halfway. trap restore_backend EXIT - just -f "{{source_file()}}" _backend-down "{{context}}" - just -f "{{source_file()}}" _wipe-db "{{context}}" + just -f "{{source_file()}}" _backend-down "{{instance}}" "{{context}}" + just -f "{{source_file()}}" _wipe-db "{{instance}}" "{{context}}" # Back up, wipe, then seed the dev fixture from this checkout. [group('cluster')] -reseed context: +reseed instance context: #!/usr/bin/env bash set -euo pipefail + {{instances}} + resolve_instance "{{instance}}" cd "{{root_dir}}" - just -f "{{source_file()}}" _backend-matches-main - just -f "{{source_file()}}" _guard "{{context}}" + just -f "{{source_file()}}" _backend-matches "{{instance}}" + just -f "{{source_file()}}" _guard "{{instance}}" "{{context}}" - kc=(kubectl --context "{{context}}" -n "{{namespace}}") + kc=(kubectl --context "{{context}}" -n "$ns") tmp="$(mktemp -d)" pf_pid="" backend_down="" @@ -136,7 +153,7 @@ reseed context: trap cleanup EXIT # Everything that can fail without changing the cluster happens first, so - # a failure here leaves dev exactly as it was. + # a failure here leaves the instance exactly as it was. echo "==> Building the seed" (cd components/backend && go build -o "$tmp/seed" ./cmd/seed/) @@ -150,17 +167,17 @@ reseed context: replicas="$("${kc[@]}" get "deploy/{{backend_deploy}}" -o jsonpath='{.spec.replicas}')" [ "${replicas:-0}" -gt 0 ] || replicas=1 - just -f "{{source_file()}}" _confirm "{{context}}" "WIPE and RESEED the app database" - just -f "{{source_file()}}" backup "{{context}}" + just -f "{{source_file()}}" _confirm "{{instance}}" "{{context}}" "WIPE and RESEED the app database" + just -f "{{source_file()}}" backup "{{instance}}" "{{context}}" # The backend stays off until the seed is done: started on the empty # database it would create the tables at the same time as the seed, and it # would load its casbin policy before the seed has written any. backend_down=yes - just -f "{{source_file()}}" _backend-down "{{context}}" + just -f "{{source_file()}}" _backend-down "{{instance}}" "{{context}}" # Wipe the app database, leaving keycloak untouched. The seed creates the schema. - just -f "{{source_file()}}" _wipe-db "{{context}}" + just -f "{{source_file()}}" _wipe-db "{{instance}}" "{{context}}" echo "==> Port-forwarding postgres to localhost:{{local_pg_port}}" "${kc[@]}" port-forward "svc/{{postgres_svc}}" "{{local_pg_port}}:5432" >/dev/null & @@ -186,13 +203,15 @@ reseed context: # the caller's job, in an EXIT trap set before calling, so it happens even when # this or a later step fails halfway. [private] -_backend-down context: +_backend-down instance context: #!/usr/bin/env bash set -euo pipefail + {{instances}} + resolve_instance "{{instance}}" - # Guard the context, so a recipe pointed at the prod context by mistake stops before touching anything. - just -f "{{source_file()}}" _guard "{{context}}" - kc=(kubectl --context "{{context}}" -n "{{namespace}}") + # Guard the context, so a recipe pointed at the wrong cluster by mistake stops before touching anything. + just -f "{{source_file()}}" _guard "{{instance}}" "{{context}}" + kc=(kubectl --context "{{context}}" -n "$ns") backend_pods() { "${kc[@]}" get pods -o name | grep "{{backend_deploy}}-" || true; } @@ -210,13 +229,15 @@ _backend-down context: # Drop and recreate the app database and run _backend-down first, # so the backend does not try to connect to it while it is being replaced. [private] -_wipe-db context: +_wipe-db instance context: #!/usr/bin/env bash set -euo pipefail + {{instances}} + resolve_instance "{{instance}}" - # Guard the context, so a recipe pointed at the prod context by mistake stops before touching anything. - just -f "{{source_file()}}" _guard "{{context}}" - kc=(kubectl --context "{{context}}" -n "{{namespace}}") + # Guard the context, so a recipe pointed at the wrong cluster by mistake stops before touching anything. + just -f "{{source_file()}}" _guard "{{instance}}" "{{context}}" + kc=(kubectl --context "{{context}}" -n "$ns") echo "==> Recreating database {{app_db}}" "${kc[@]}" exec "sts/{{postgres_sts}}" -- sh -c '{{pg_as_superuser}}' sh \ @@ -225,47 +246,66 @@ _wipe-db context: -c 'CREATE DATABASE {{app_db}} OWNER {{app_db_owner}};' echo "✓ Database {{app_db}} is empty" -# Refuse unless this checkout's backend is the code dev serves. +# Refuse unless this checkout's backend is the code the instance serves: dev +# follows origin/main, staging the newest origin/release/* branch. [private] -_backend-matches-main: +_backend-matches instance: #!/usr/bin/env bash set -euo pipefail cd "{{root_dir}}" - git fetch --quiet origin main - if ! git merge-base --is-ancestor origin/main HEAD; then - echo "✗ This checkout is behind origin/main. Rebase or merge main first." >&2 + case "{{instance}}" in + dev) + git fetch --quiet origin main + ref=origin/main + ;; + staging) + git fetch --quiet --prune origin '+refs/heads/release/*:refs/remotes/origin/release/*' + ref="$(git for-each-ref --sort=-version:refname --count=1 \ + --format='%(refname:short)' refs/remotes/origin/release/)" + [ -n "$ref" ] || { echo "✗ origin has no release/* branch." >&2; exit 1; } + ;; + *) echo "✗ Unknown instance '{{instance}}': expected dev or staging." >&2; exit 1 ;; + esac + + if ! git merge-base --is-ancestor "$ref" HEAD; then + echo "✗ This checkout is behind $ref. Check out or merge $ref first." >&2 exit 1 fi - if ! git diff --quiet origin/main -- components/backend; then - echo "✗ components/backend differs from origin/main, which dev is serving." >&2 + if ! git diff --quiet "$ref" -- components/backend; then + echo "✗ components/backend differs from $ref, which {{instance}} is serving." >&2 exit 1 fi if [ -n "$(git status --porcelain components/backend)" ]; then echo "✗ components/backend has uncommitted changes." >&2 exit 1 fi - echo "✓ components/backend matches origin/main" + echo "✓ components/backend matches $ref" -# Refuse a context whose hackagon ingress does not serve the dev domain. +# Refuse a context whose instance namespace has no ingress for the instance's host. [private] -_guard context: +_guard instance context: #!/usr/bin/env bash set -euo pipefail - hosts="$(kubectl --context "{{context}}" -n "{{namespace}}" get ingress \ + {{instances}} + resolve_instance "{{instance}}" + hosts="$(kubectl --context "{{context}}" -n "$ns" get ingress \ -o jsonpath='{.items[*].spec.rules[*].host}')" - if [[ " $hosts " != *"{{dev_domain}}"* ]]; then - echo "✗ Context '{{context}}' does not serve {{dev_domain}} (found: ${hosts:-none})." >&2 - echo " Refusing — this does not look like the dev instance." >&2 + if [[ " $hosts " != *" $host "* ]]; then + echo "✗ Namespace $ns on context '{{context}}' does not serve $host (found: ${hosts:-none})." >&2 + echo " Refusing — this does not look like the {{instance}} instance." >&2 exit 1 fi -# Guard, then make the operator type the context name back. +# Guard, then make the operator type the namespace back. The namespace, not the +# context, because dev and staging share a context. [private] -_confirm context action: +_confirm instance context action: #!/usr/bin/env bash set -euo pipefail - just -f "{{source_file()}}" _guard "{{context}}" - echo "About to {{action}} on context '{{context}}' (namespace {{namespace}})." + {{instances}} + resolve_instance "{{instance}}" + just -f "{{source_file()}}" _guard "{{instance}}" "{{context}}" + echo "About to {{action}} on context '{{context}}' (namespace $ns)." echo "Everything in the app database is lost; a backup is taken first." - read -r -p "Type the context name to continue: " answer - [ "$answer" = "{{context}}" ] || { echo "Aborted."; exit 1; } + read -r -p "Type the namespace to continue: " answer + [ "$answer" = "$ns" ] || { echo "Aborted."; exit 1; } From effb114fa87f094eef43cc7be8e2984b7bb71495 Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:06:23 +0100 Subject: [PATCH 2/8] feat(tooling): reach dev and staging through one kubeconfig Both instances live in the cluster behind ~/.config/kube/sck-sit-dev, so the recipes take that file (override with HACKAGON_KUBECONFIG) instead of a context argument: `just cluster::reseed staging`. --- tools/just/cluster.just | 92 ++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/tools/just/cluster.just b/tools/just/cluster.just index 8bd51935..70ce5a54 100644 --- a/tools/just/cluster.just +++ b/tools/just/cluster.just @@ -16,10 +16,14 @@ app_db_owner := "hackagon" backup_dir := env("HOME") + "/hackagon-backups" local_pg_port := "15432" +# Dev and staging share one cluster, reached through this kubeconfig and its +# current context. Override with HACKAGON_KUBECONFIG. +kubeconfig := env("HACKAGON_KUBECONFIG", env("HOME") + "/.config/kube/sck-sit-dev") + # Sourced by every recipe: `resolve_instance ` sets the namespace and the # host its ingress must serve. Only these two names exist, so a recipe can never -# be pointed at prod, and the host check catches a context that is not the one -# holding them. +# be pointed at prod, and the host check catches a kubeconfig that reaches some +# other cluster. instances := ''' resolve_instance() { case "$1" in @@ -55,55 +59,55 @@ help: echo "" echo -e " ${bold}Cluster Commands${reset}" - echo -e " ${dim}Act on a deployed instance — dev or staging, never prod. Every recipe${reset}" - echo -e " ${dim}takes the instance and the kube context explicitly, and refuses a${reset}" - echo -e " ${dim}namespace whose ingress does not serve that instance's host.${reset}" + echo -e " ${dim}Act on a deployed instance — dev or staging, never prod — through${reset}" + echo -e " ${dim}{{kubeconfig}} (override with HACKAGON_KUBECONFIG).${reset}" + echo -e " ${dim}Every recipe refuses a namespace whose ingress does not serve that instance's host.${reset}" echo "" - echo -e " ${cyan}just cluster::backup${reset} " + echo -e " ${cyan}just cluster::backup${reset} " echo -e " ${dim}pg_dumpall the instance's postgres to {{backup_dir}}${reset}" echo "" - echo -e " ${cyan}just cluster::wipe${reset} " + echo -e " ${cyan}just cluster::wipe${reset} " echo -e " ${dim}Back up, then drop and recreate the app database. Keycloak is untouched.${reset}" echo -e " ${dim}The backend recreates the schema on startup, so the instance comes back empty.${reset}" echo "" - echo -e " ${cyan}just cluster::reseed${reset} " + echo -e " ${cyan}just cluster::reseed${reset} " echo -e " ${dim}Back up, wipe, then run cmd/seed. components/backend must match what the${reset}" echo -e " ${dim}instance serves: origin/main for dev, the newest origin/release/* for staging.${reset}" echo "" - echo -e " ${dim}e.g. just cluster::reseed staging sck-sit-dev${reset}" + echo -e " ${dim}e.g. just cluster::reseed staging${reset}" echo "" # pg_dumpall the instance's postgres (app and keycloak databases) to ~/hackagon-backups. [group('cluster')] -backup instance context: +backup instance: #!/usr/bin/env bash set -euo pipefail {{instances}} resolve_instance "{{instance}}" - just -f "{{source_file()}}" _guard "{{instance}}" "{{context}}" + just -f "{{source_file()}}" _guard "{{instance}}" mkdir -p "{{backup_dir}}" - out="{{backup_dir}}/{{context}}-{{instance}}-$(date +%Y%m%d-%H%M%S).sql" + out="{{backup_dir}}/$ns-$(date +%Y%m%d-%H%M%S).sql" discard_partial() { rm -f "$out.partial"; } trap discard_partial EXIT echo "==> Backing up to $out" - kubectl --context "{{context}}" -n "$ns" exec "sts/{{postgres_sts}}" -- \ + kubectl --kubeconfig "{{kubeconfig}}" -n "$ns" exec "sts/{{postgres_sts}}" -- \ sh -c '{{pg_as_superuser}}' sh pg_dumpall -h 127.0.0.1 -U postgres > "$out.partial" mv "$out.partial" "$out" echo "✓ $(du -h "$out" | cut -f1) written" # Back up, then drop and recreate the app database; the instance restarts empty. [group('cluster')] -wipe instance context: +wipe instance: #!/usr/bin/env bash set -euo pipefail {{instances}} resolve_instance "{{instance}}" - just -f "{{source_file()}}" _confirm "{{instance}}" "{{context}}" "WIPE the app database" - just -f "{{source_file()}}" backup "{{instance}}" "{{context}}" + just -f "{{source_file()}}" _confirm "{{instance}}" "WIPE the app database" + just -f "{{source_file()}}" backup "{{instance}}" - kc=(kubectl --context "{{context}}" -n "$ns") + kc=(kubectl --kubeconfig "{{kubeconfig}}" -n "$ns") # A backend found at 0 (e.g. left there by an interrupted run) comes back # at 1: both recipes promise a running instance, and wipe needs it to # recreate the schema. @@ -118,12 +122,12 @@ wipe instance context: # however this ends, including when stopping it or the wipe fails halfway. trap restore_backend EXIT - just -f "{{source_file()}}" _backend-down "{{instance}}" "{{context}}" - just -f "{{source_file()}}" _wipe-db "{{instance}}" "{{context}}" + just -f "{{source_file()}}" _backend-down "{{instance}}" + just -f "{{source_file()}}" _wipe-db "{{instance}}" # Back up, wipe, then seed the dev fixture from this checkout. [group('cluster')] -reseed instance context: +reseed instance: #!/usr/bin/env bash set -euo pipefail {{instances}} @@ -131,9 +135,9 @@ reseed instance context: cd "{{root_dir}}" just -f "{{source_file()}}" _backend-matches "{{instance}}" - just -f "{{source_file()}}" _guard "{{instance}}" "{{context}}" + just -f "{{source_file()}}" _guard "{{instance}}" - kc=(kubectl --context "{{context}}" -n "$ns") + kc=(kubectl --kubeconfig "{{kubeconfig}}" -n "$ns") tmp="$(mktemp -d)" pf_pid="" backend_down="" @@ -167,17 +171,17 @@ reseed instance context: replicas="$("${kc[@]}" get "deploy/{{backend_deploy}}" -o jsonpath='{.spec.replicas}')" [ "${replicas:-0}" -gt 0 ] || replicas=1 - just -f "{{source_file()}}" _confirm "{{instance}}" "{{context}}" "WIPE and RESEED the app database" - just -f "{{source_file()}}" backup "{{instance}}" "{{context}}" + just -f "{{source_file()}}" _confirm "{{instance}}" "WIPE and RESEED the app database" + just -f "{{source_file()}}" backup "{{instance}}" # The backend stays off until the seed is done: started on the empty # database it would create the tables at the same time as the seed, and it # would load its casbin policy before the seed has written any. backend_down=yes - just -f "{{source_file()}}" _backend-down "{{instance}}" "{{context}}" + just -f "{{source_file()}}" _backend-down "{{instance}}" # Wipe the app database, leaving keycloak untouched. The seed creates the schema. - just -f "{{source_file()}}" _wipe-db "{{instance}}" "{{context}}" + just -f "{{source_file()}}" _wipe-db "{{instance}}" echo "==> Port-forwarding postgres to localhost:{{local_pg_port}}" "${kc[@]}" port-forward "svc/{{postgres_svc}}" "{{local_pg_port}}:5432" >/dev/null & @@ -203,15 +207,15 @@ reseed instance context: # the caller's job, in an EXIT trap set before calling, so it happens even when # this or a later step fails halfway. [private] -_backend-down instance context: +_backend-down instance: #!/usr/bin/env bash set -euo pipefail {{instances}} resolve_instance "{{instance}}" - # Guard the context, so a recipe pointed at the wrong cluster by mistake stops before touching anything. - just -f "{{source_file()}}" _guard "{{instance}}" "{{context}}" - kc=(kubectl --context "{{context}}" -n "$ns") + # Guard the instance, so a kubeconfig reaching the wrong cluster stops before touching anything. + just -f "{{source_file()}}" _guard "{{instance}}" + kc=(kubectl --kubeconfig "{{kubeconfig}}" -n "$ns") backend_pods() { "${kc[@]}" get pods -o name | grep "{{backend_deploy}}-" || true; } @@ -229,15 +233,15 @@ _backend-down instance context: # Drop and recreate the app database and run _backend-down first, # so the backend does not try to connect to it while it is being replaced. [private] -_wipe-db instance context: +_wipe-db instance: #!/usr/bin/env bash set -euo pipefail {{instances}} resolve_instance "{{instance}}" - # Guard the context, so a recipe pointed at the wrong cluster by mistake stops before touching anything. - just -f "{{source_file()}}" _guard "{{instance}}" "{{context}}" - kc=(kubectl --context "{{context}}" -n "$ns") + # Guard the instance, so a kubeconfig reaching the wrong cluster stops before touching anything. + just -f "{{source_file()}}" _guard "{{instance}}" + kc=(kubectl --kubeconfig "{{kubeconfig}}" -n "$ns") echo "==> Recreating database {{app_db}}" "${kc[@]}" exec "sts/{{postgres_sts}}" -- sh -c '{{pg_as_superuser}}' sh \ @@ -281,31 +285,33 @@ _backend-matches instance: fi echo "✓ components/backend matches $ref" -# Refuse a context whose instance namespace has no ingress for the instance's host. +# Refuse unless the kubeconfig reaches a namespace whose ingress serves the instance's host. [private] -_guard instance context: +_guard instance: #!/usr/bin/env bash set -euo pipefail {{instances}} resolve_instance "{{instance}}" - hosts="$(kubectl --context "{{context}}" -n "$ns" get ingress \ + [ -f "{{kubeconfig}}" ] \ + || { echo "✗ No kubeconfig at {{kubeconfig}}. Set HACKAGON_KUBECONFIG." >&2; exit 1; } + hosts="$(kubectl --kubeconfig "{{kubeconfig}}" -n "$ns" get ingress \ -o jsonpath='{.items[*].spec.rules[*].host}')" if [[ " $hosts " != *" $host "* ]]; then - echo "✗ Namespace $ns on context '{{context}}' does not serve $host (found: ${hosts:-none})." >&2 + echo "✗ Namespace $ns via {{kubeconfig}} does not serve $host (found: ${hosts:-none})." >&2 echo " Refusing — this does not look like the {{instance}} instance." >&2 exit 1 fi -# Guard, then make the operator type the namespace back. The namespace, not the -# context, because dev and staging share a context. +# Guard, then make the operator type the namespace back, which names the +# instance; dev and staging share a cluster. [private] -_confirm instance context action: +_confirm instance action: #!/usr/bin/env bash set -euo pipefail {{instances}} resolve_instance "{{instance}}" - just -f "{{source_file()}}" _guard "{{instance}}" "{{context}}" - echo "About to {{action}} on context '{{context}}' (namespace $ns)." + just -f "{{source_file()}}" _guard "{{instance}}" + echo "About to {{action}} in namespace $ns (via {{kubeconfig}})." echo "Everything in the app database is lost; a backup is taken first." read -r -p "Type the namespace to continue: " answer [ "$answer" = "$ns" ] || { echo "Aborted."; exit 1; } From c0fdf8f84faa888006ae88f9259373f94e9f0d36 Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:06:23 +0100 Subject: [PATCH 3/8] feat(tooling): build the reseed from the code the instance serves reseed builds cmd/seed in a temporary worktree at origin/main for dev and the newest origin/release/* for staging, so staging can be reseeded from a main checkout whose recipes know about it, and no checkout state matters. --- tools/just/cluster.just | 43 ++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/tools/just/cluster.just b/tools/just/cluster.just index 70ce5a54..e046c03d 100644 --- a/tools/just/cluster.just +++ b/tools/just/cluster.just @@ -71,8 +71,8 @@ help: echo -e " ${dim}The backend recreates the schema on startup, so the instance comes back empty.${reset}" echo "" echo -e " ${cyan}just cluster::reseed${reset} " - echo -e " ${dim}Back up, wipe, then run cmd/seed. components/backend must match what the${reset}" - echo -e " ${dim}instance serves: origin/main for dev, the newest origin/release/* for staging.${reset}" + echo -e " ${dim}Back up, wipe, then run cmd/seed built from the code the instance serves —${reset}" + echo -e " ${dim}origin/main for dev, the newest origin/release/* for staging — whatever is checked out.${reset}" echo "" echo -e " ${dim}e.g. just cluster::reseed staging${reset}" echo "" @@ -125,7 +125,7 @@ wipe instance: just -f "{{source_file()}}" _backend-down "{{instance}}" just -f "{{source_file()}}" _wipe-db "{{instance}}" -# Back up, wipe, then seed the dev fixture from this checkout. +# Back up, wipe, then seed the dev fixture, built from the code the instance serves. [group('cluster')] reseed instance: #!/usr/bin/env bash @@ -134,7 +134,7 @@ reseed instance: resolve_instance "{{instance}}" cd "{{root_dir}}" - just -f "{{source_file()}}" _backend-matches "{{instance}}" + ref="$(just -f "{{source_file()}}" _seed-ref "{{instance}}")" just -f "{{source_file()}}" _guard "{{instance}}" kc=(kubectl --kubeconfig "{{kubeconfig}}" -n "$ns") @@ -147,19 +147,23 @@ reseed instance: echo "==> Scaling backend back to $replicas" "${kc[@]}" scale "deploy/{{backend_deploy}}" --replicas="$replicas" } - # However this ends: close the tunnel, delete the temp dir, and bring the - # backend back if it was taken down. + # However this ends: close the tunnel, delete the temp dir and the worktree + # in it, and bring the backend back if it was taken down. cleanup() { [ -n "$pf_pid" ] && kill "$pf_pid" 2>/dev/null || true rm -rf "$tmp" + git -C "{{root_dir}}" worktree prune [ -z "$backend_down" ] || restore_backend } trap cleanup EXIT # Everything that can fail without changing the cluster happens first, so # a failure here leaves the instance exactly as it was. - echo "==> Building the seed" - (cd components/backend && go build -o "$tmp/seed" ./cmd/seed/) + # The seed runs the backend's handlers in-process and creates the schema, so + # it is built from the code the instance serves, not from this checkout. + echo "==> Building the seed from $ref" + git worktree add --quiet --detach "$tmp/src" "$ref" + (cd "$tmp/src/components/backend" && go build -o "$tmp/seed" ./cmd/seed/) echo "==> Fetching backend config and database password from the cluster" "${kc[@]}" get cm "{{backend_config}}" -o jsonpath='{.data.config\.yaml}' > "$tmp/config.yaml" @@ -196,7 +200,7 @@ reseed instance: || { echo "✗ The port-forward to postgres did not open within 10s." >&2; exit 1; } echo "==> Seeding" - cd components/backend + cd "$tmp/src/components/backend" HACKAGON_DATABASE_HOST=127.0.0.1 \ HACKAGON_DATABASE_PORT="{{local_pg_port}}" \ HACKAGON_DATABASE_PASSWORD="$db_password" \ @@ -250,10 +254,10 @@ _wipe-db instance: -c 'CREATE DATABASE {{app_db}} OWNER {{app_db_owner}};' echo "✓ Database {{app_db}} is empty" -# Refuse unless this checkout's backend is the code the instance serves: dev -# follows origin/main, staging the newest origin/release/* branch. +# Fetch and print the ref whose code the instance serves: dev follows +# origin/main, staging the newest origin/release/* branch. [private] -_backend-matches instance: +_seed-ref instance: #!/usr/bin/env bash set -euo pipefail cd "{{root_dir}}" @@ -270,20 +274,7 @@ _backend-matches instance: ;; *) echo "✗ Unknown instance '{{instance}}': expected dev or staging." >&2; exit 1 ;; esac - - if ! git merge-base --is-ancestor "$ref" HEAD; then - echo "✗ This checkout is behind $ref. Check out or merge $ref first." >&2 - exit 1 - fi - if ! git diff --quiet "$ref" -- components/backend; then - echo "✗ components/backend differs from $ref, which {{instance}} is serving." >&2 - exit 1 - fi - if [ -n "$(git status --porcelain components/backend)" ]; then - echo "✗ components/backend has uncommitted changes." >&2 - exit 1 - fi - echo "✓ components/backend matches $ref" + echo "$ref" # Refuse unless the kubeconfig reaches a namespace whose ingress serves the instance's host. [private] From 03eb460e2a36e211828ad1b2c2130c888741da9f Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:25:20 +0100 Subject: [PATCH 4/8] docs: dev and staging instances Dev (hackagon-dev) follows main, staging (hackagon-staging) runs the latest release; both share one cluster. Describe the three instances, how dev and staging get test data, and the new cluster:: arguments. --- .claude/skills/backend-seeding/SKILL.md | 13 +++++---- CLAUDE.md | 18 +++++++------ RELEASING.md | 36 ++++++++++++++++++------- components/backend/cmd/seed/README.md | 18 +++++++------ 4 files changed, 54 insertions(+), 31 deletions(-) diff --git a/.claude/skills/backend-seeding/SKILL.md b/.claude/skills/backend-seeding/SKILL.md index fff24847..9eaff362 100644 --- a/.claude/skills/backend-seeding/SKILL.md +++ b/.claude/skills/backend-seeding/SKILL.md @@ -5,8 +5,8 @@ description: or changing hackathons, users, projects, teams, submissions, phases and votes through the in-process gRPC harness, the step helpers in steps.go, capabilities that must be on when they are used, synthetic users who cannot - log in, idempotency and re-seeding (locally and on the dev cluster), and how - to verify a change without wiping local state. Use when asked to add or change + log in, idempotency and re-seeding (locally and on dev or staging), and how to + verify a change without wiping local state. Use when asked to add or change dev/test data, make a scenario reproducible locally, pick which seeded hackathon to test something in, or fix a seeded hackathon nobody can act in. --- @@ -151,9 +151,12 @@ To use the new fixture locally for real: `just clean::state && just start`, policy before the seed wrote any, and until it restarts, owners are refused their own hackathons. -On the dev cluster, `just cluster::reseed ` backs up, wipes the app -database, seeds from your checkout and restarts the backend. It refuses unless -`components/backend` matches `origin/main`, so it runs after merging. +On the deployed instances, `just cluster::reseed dev` or +`just cluster::reseed staging` backs up, wipes the app database, seeds and +restarts the backend. The seed is built from the code the instance serves — +`origin/main` for dev, the newest `origin/release/*` for staging — not from your +checkout, so a seed change reaches dev only after merging and staging only once +it is on a release branch. ## After changing the seed diff --git a/CLAUDE.md b/CLAUDE.md index bdce84dc..8c58acb9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,10 +113,10 @@ just helm::template [args] # render the chart to stdout just helm::check-bump # fail if helm-chart/ changed without a version bump just helm::publish # push the chart if Chart.yaml's version is unpublished -# Deployed dev instance only — takes the kube context, refuses any other cluster -just cluster::backup # pg_dumpall to ~/hackagon-backups -just cluster::wipe # backup, then drop + recreate the app DB; dev restarts empty -just cluster::reseed # backup, wipe, then seed; refuses if components/backend differs from origin/main +# Deployed dev or staging only, via ~/.config/kube/sck-sit-dev (override: HACKAGON_KUBECONFIG) +just cluster::backup # pg_dumpall to ~/hackagon-backups +just cluster::wipe # backup, then drop + recreate the app DB; the instance restarts empty +just cluster::reseed # backup, wipe, then seed built from origin/main (dev) or the newest origin/release/* (staging) ``` Backend listens on **:3000**, frontend on **:8081**. Dev users (Keycloak @@ -169,10 +169,12 @@ These hold across the whole codebase; the skills explain the mechanisms. focused pull request per change, each carrying its manually added `CHANGELOG.md` entry. Each released minor line has a `release/X.Y` branch that takes hotfixes only, released from its own tags and cherry-picked back to - `main`. Both clusters install the same chart from the GitOps setup in - `sdsc-ordes/cloud-infra` and differ only in image overrides: **dev** runs - `temporary/*:latest` with `pullPolicy: Always`, so every push to `main` - reaches it on the next container restart; **prod** takes the chart defaults, + `main`. All three instances install the same chart from the GitOps setup in + `sdsc-ordes/cloud-infra` and differ only in image overrides: **dev** + (namespace `hackagon-dev`) runs `temporary/*:latest` with + `pullPolicy: Always`, so every push to `main` reaches it on the next container + restart; **staging** (namespace `hackagon-staging`, same cluster as dev) runs + the latest release, hotfixes included; **prod** takes the chart defaults, `release/*` at `appVersion`, so nothing merged can reach it until someone bumps `appVersion` and installs the new chart version. See `RELEASING.md`. - **The chart versions itself; `VERSION` never touches it.** diff --git a/RELEASING.md b/RELEASING.md index eba8cb06..1760b8a9 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -15,7 +15,7 @@ main ──●──●──●──●──●──●──●──●─ │ v0.12.1 + its chart bump │ - └── release/0.12 ──●── v0.12.2 ──●── v0.12.3 hotfixes only; prod runs these tags + └── release/0.12 ──●── v0.12.2 ──●── v0.12.3 hotfixes only; staging and prod run these tags ▲ hotfix/… PRs ``` @@ -44,14 +44,24 @@ itself builds nothing; only its tags do. ## Deployment -Two clusters, both installed from the Helm chart by the GitOps setup in -`sdsc-ordes/cloud-infra` — a base `values.yaml` plus per-cluster overrides. The -only thing that differs between them is which images they pull. - -| Cluster | Images | Moves when | -| ------- | -------------------------------------------- | -------------------------------- | -| dev | `temporary/*:latest`, `pullPolicy: Always` | anything is merged to `main` | -| prod | chart defaults — `release/*` at `appVersion` | a new chart version is installed | +Three instances, all installed from the Helm chart by the GitOps setup in +`sdsc-ordes/cloud-infra` — a base `values.yaml` plus per-instance overrides. The +only thing that differs between them is which images they pull. Dev and staging +share one cluster, each in its own namespace, reached through the kubeconfig +`~/.config/kube/sck-sit-dev`. + +| Instance | Namespace | Images | Moves when | +| ------------------------------------------------------------- | ------------------ | -------------------------------------------- | -------------------------------- | +| [dev](https://hackagon-dev.sck-sit-dev.dscompute.ch/) | `hackagon-dev` | `temporary/*:latest`, `pullPolicy: Always` | anything is merged to `main` | +| [staging](https://hackagon-staging.sck-sit-dev.dscompute.ch/) | `hackagon-staging` | the latest release, hotfixes included | a new release or hotfix is out | +| prod | | chart defaults — `release/*` at `appVersion` | a new chart version is installed | + +None of them seeds itself. Dev and staging get the test fixture from +`just cluster::reseed dev` or `just cluster::reseed staging`, which builds the +seed from the code that instance serves — `origin/main` or the newest +`origin/release/*` — whatever is checked out; see +[the seed README](components/backend/cmd/seed/README.md#when-to-re-seed). Prod +is never seeded, and the `cluster::` recipes cannot name it. ### dev follows `main` @@ -64,7 +74,8 @@ A restart is not always something you chose: an eviction or a node drain restarts the container too. To restart it on purpose: ```bash -kubectl rollout restart deploy/hackagon-frontend +kubectl --kubeconfig ~/.config/kube/sck-sit-dev -n hackagon-dev \ + rollout restart deploy/hackagon-frontend ``` To see which build it is serving, read the version at the bottom of the page. @@ -84,6 +95,11 @@ overwritten; those never do, so a specific build can be pinned: `skopeo list-tags docker://` shows which tags exist. +### staging runs the latest release + +Staging runs the newest release, hotfixes included: what prod would get on its +next install, while prod itself stays on the release it was installed with. + ### prod follows releases No image overrides at all. With `tag: ""` the chart falls back to `appVersion`, diff --git a/components/backend/cmd/seed/README.md b/components/backend/cmd/seed/README.md index 69e41c2d..3f817db1 100644 --- a/components/backend/cmd/seed/README.md +++ b/components/backend/cmd/seed/README.md @@ -53,10 +53,10 @@ cannot log in — see [Users](#users). ## When to re-seed **Nothing re-seeds automatically.** The backend never runs `cmd/seed`: a deploy, -a restart or a new image only migrates the schema and keeps every row. So the -dev cluster holds whatever the last manual re-seed wrote, plus everything people -have done to it since; production is never seeded at all. CI does run the seed, -but against a throwaway stack, to prove it still works — nothing it writes +a restart or a new image only migrates the schema and keeps every row. So dev +and staging hold whatever the last manual re-seed wrote, plus everything people +have done to them since; production is never seeded at all. CI does run the +seed, but against a throwaway stack, to prove it still works — nothing it writes survives the job. Re-seed when one of these is true: @@ -95,10 +95,12 @@ just deploy::down && just deploy::up # reload the casbin policy, see below # Or all of that at once, regenerating code too just schema-change -# The dev cluster — backs up, wipes the app database (Keycloak is kept), -# seeds from your checkout and restarts the backend. Refuses unless -# components/backend matches origin/main. -just cluster::reseed +# Dev or staging — backs up, wipes the app database (Keycloak is kept), seeds +# and restarts the backend. The seed is built from the code the instance: +# origin/main for dev, the newest origin/release/* for staging, +# needs HACKAGON_KUBECONFIG to be set. +just cluster::reseed dev +just cluster::reseed staging ``` ## Running the seed From 14b3abe7f9b98c4b2f5ce00ed590933376c255d4 Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Thu, 5 Nov 2026 12:46:44 +0100 Subject: [PATCH 5/8] fix: correct kubeconfig: add as env variable --- CLAUDE.md | 2 +- RELEASING.md | 4 ++-- tools/just/cluster.just | 15 +++++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8c58acb9..5f0e3a3e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,7 +113,7 @@ just helm::template [args] # render the chart to stdout just helm::check-bump # fail if helm-chart/ changed without a version bump just helm::publish # push the chart if Chart.yaml's version is unpublished -# Deployed dev or staging only, via ~/.config/kube/sck-sit-dev (override: HACKAGON_KUBECONFIG) +# Deployed dev or staging only — set HACKAGON_KUBECONFIG to the cluster's kubeconfig first just cluster::backup # pg_dumpall to ~/hackagon-backups just cluster::wipe # backup, then drop + recreate the app DB; the instance restarts empty just cluster::reseed # backup, wipe, then seed built from origin/main (dev) or the newest origin/release/* (staging) diff --git a/RELEASING.md b/RELEASING.md index 1760b8a9..08dd2e17 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -48,7 +48,7 @@ Three instances, all installed from the Helm chart by the GitOps setup in `sdsc-ordes/cloud-infra` — a base `values.yaml` plus per-instance overrides. The only thing that differs between them is which images they pull. Dev and staging share one cluster, each in its own namespace, reached through the kubeconfig -`~/.config/kube/sck-sit-dev`. +that `HACKAGON_KUBECONFIG` points at. | Instance | Namespace | Images | Moves when | | ------------------------------------------------------------- | ------------------ | -------------------------------------------- | -------------------------------- | @@ -74,7 +74,7 @@ A restart is not always something you chose: an eviction or a node drain restarts the container too. To restart it on purpose: ```bash -kubectl --kubeconfig ~/.config/kube/sck-sit-dev -n hackagon-dev \ +kubectl --kubeconfig "$HACKAGON_KUBECONFIG" -n hackagon-dev \ rollout restart deploy/hackagon-frontend ``` diff --git a/tools/just/cluster.just b/tools/just/cluster.just index e046c03d..e18a04ef 100644 --- a/tools/just/cluster.just +++ b/tools/just/cluster.just @@ -16,9 +16,10 @@ app_db_owner := "hackagon" backup_dir := env("HOME") + "/hackagon-backups" local_pg_port := "15432" -# Dev and staging share one cluster, reached through this kubeconfig and its -# current context. Override with HACKAGON_KUBECONFIG. -kubeconfig := env("HACKAGON_KUBECONFIG", env("HOME") + "/.config/kube/sck-sit-dev") +# Dev and staging share one cluster, reached through the kubeconfig that +# HACKAGON_KUBECONFIG points at, and its current context. No default: where +# that file lives is each operator's business, and _guard refuses when unset. +kubeconfig := env("HACKAGON_KUBECONFIG", "") # Sourced by every recipe: `resolve_instance ` sets the namespace and the # host its ingress must serve. Only these two names exist, so a recipe can never @@ -59,8 +60,8 @@ help: echo "" echo -e " ${bold}Cluster Commands${reset}" - echo -e " ${dim}Act on a deployed instance — dev or staging, never prod — through${reset}" - echo -e " ${dim}{{kubeconfig}} (override with HACKAGON_KUBECONFIG).${reset}" + echo -e " ${dim}Act on a deployed instance — dev or staging, never prod — through the${reset}" + echo -e " ${dim}kubeconfig HACKAGON_KUBECONFIG points at.${reset}" echo -e " ${dim}Every recipe refuses a namespace whose ingress does not serve that instance's host.${reset}" echo "" echo -e " ${cyan}just cluster::backup${reset} " @@ -283,8 +284,10 @@ _guard instance: set -euo pipefail {{instances}} resolve_instance "{{instance}}" + [ -n "{{kubeconfig}}" ] \ + || { echo "✗ HACKAGON_KUBECONFIG is not set. Point it at the kubeconfig for dev and staging." >&2; exit 1; } [ -f "{{kubeconfig}}" ] \ - || { echo "✗ No kubeconfig at {{kubeconfig}}. Set HACKAGON_KUBECONFIG." >&2; exit 1; } + || { echo "✗ HACKAGON_KUBECONFIG points at {{kubeconfig}}, which is not a file." >&2; exit 1; } hosts="$(kubectl --kubeconfig "{{kubeconfig}}" -n "$ns" get ingress \ -o jsonpath='{.items[*].spec.rules[*].host}')" if [[ " $hosts " != *" $host "* ]]; then From 7331141e6c7d909a2d0b9b366a65e3da778daba9 Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Thu, 5 Nov 2026 13:02:53 +0100 Subject: [PATCH 6/8] fix: correct spelling in relase documentation --- RELEASING.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 08dd2e17..f35ef53a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -59,7 +59,7 @@ that `HACKAGON_KUBECONFIG` points at. None of them seeds itself. Dev and staging get the test fixture from `just cluster::reseed dev` or `just cluster::reseed staging`, which builds the seed from the code that instance serves — `origin/main` or the newest -`origin/release/*` — whatever is checked out; see +`origin/release/*`. See [the seed README](components/backend/cmd/seed/README.md#when-to-re-seed). Prod is never seeded, and the `cluster::` recipes cannot name it. @@ -97,8 +97,7 @@ overwritten; those never do, so a specific build can be pinned: ### staging runs the latest release -Staging runs the newest release, hotfixes included: what prod would get on its -next install, while prod itself stays on the release it was installed with. +Staging runs the newest release, hotfixes included: it is there to test hotfixes before they go to prod. ### prod follows releases From 8e5f32c19594d43eb241253b9f17574a8fb3e7e2 Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Thu, 5 Nov 2026 13:18:03 +0100 Subject: [PATCH 7/8] chore: fix formatting markdown --- RELEASING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index f35ef53a..abbba289 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -97,7 +97,8 @@ overwritten; those never do, so a specific build can be pinned: ### staging runs the latest release -Staging runs the newest release, hotfixes included: it is there to test hotfixes before they go to prod. +Staging runs the newest release, hotfixes included: it is there to test hotfixes +before they go to prod. ### prod follows releases From 09d22276c103b81ca3322a3d12741950911b299b Mon Sep 17 00:00:00 2001 From: Sabine Maennel <5292683+sabinem@users.noreply.github.com> Date: Fri, 6 Nov 2026 09:27:04 +0100 Subject: [PATCH 8/8] chore: remove references to internal deployment setup --- RELEASING.md | 36 +++++++----------------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index abbba289..7e9efe6c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -46,22 +46,13 @@ itself builds nothing; only its tags do. Three instances, all installed from the Helm chart by the GitOps setup in `sdsc-ordes/cloud-infra` — a base `values.yaml` plus per-instance overrides. The -only thing that differs between them is which images they pull. Dev and staging -share one cluster, each in its own namespace, reached through the kubeconfig -that `HACKAGON_KUBECONFIG` points at. - -| Instance | Namespace | Images | Moves when | -| ------------------------------------------------------------- | ------------------ | -------------------------------------------- | -------------------------------- | -| [dev](https://hackagon-dev.sck-sit-dev.dscompute.ch/) | `hackagon-dev` | `temporary/*:latest`, `pullPolicy: Always` | anything is merged to `main` | -| [staging](https://hackagon-staging.sck-sit-dev.dscompute.ch/) | `hackagon-staging` | the latest release, hotfixes included | a new release or hotfix is out | -| prod | | chart defaults — `release/*` at `appVersion` | a new chart version is installed | - -None of them seeds itself. Dev and staging get the test fixture from -`just cluster::reseed dev` or `just cluster::reseed staging`, which builds the -seed from the code that instance serves — `origin/main` or the newest -`origin/release/*`. See -[the seed README](components/backend/cmd/seed/README.md#when-to-re-seed). Prod -is never seeded, and the `cluster::` recipes cannot name it. +only thing that differs between them is which images they pull. + +| Instance | Images | Moves when | +| -------- | -------------------------------------------- | -------------------------------- | +| dev | `temporary/*:latest`, `pullPolicy: Always` | anything is merged to `main` | +| staging | the latest release, hotfixes included | a new release or hotfix is out | +| prod | chart defaults — `release/*` at `appVersion` | a new chart version is installed | ### dev follows `main` @@ -70,14 +61,6 @@ merge to main ──► CI rebuilds temporary/*:latest ──► next container serves the new code ``` -A restart is not always something you chose: an eviction or a node drain -restarts the container too. To restart it on purpose: - -```bash -kubectl --kubeconfig "$HACKAGON_KUBECONFIG" -n hackagon-dev \ - rollout restart deploy/hackagon-frontend -``` - To see which build it is serving, read the version at the bottom of the page. `pullPolicy: Always` is what makes this work. The chart defaults to @@ -95,11 +78,6 @@ overwritten; those never do, so a specific build can be pinned: `skopeo list-tags docker://` shows which tags exist. -### staging runs the latest release - -Staging runs the newest release, hotfixes included: it is there to test hotfixes -before they go to prod. - ### prod follows releases No image overrides at all. With `tag: ""` the chart falls back to `appVersion`,