chore: upgrade project stack and deployment baseline

This commit is contained in:
SimpleTest 2026-07-18 18:55:09 +03:00
parent 68faa4795c
commit 51da6d8881
18 changed files with 330 additions and 82 deletions

View File

@ -1,9 +1,9 @@
# This file is based on these images:
#
# - https://hub.docker.com/r/hexpm/elixir/tags - for the builder image
# E.g.: docker.io/hexpm/elixir:1.18.4-erlang-27.3.4.13-debian-trixie-20260623-slim
# - https://hub.docker.com/_/debian/tags?name=trixie-20260623-slim - for the runner image
# E.g.: docker.io/debian:trixie-20260623-slim
# E.g.: docker.io/hexpm/elixir:1.20.2-erlang-29.0.3-debian-trixie-20260713-slim
# - https://hub.docker.com/_/debian/tags?name=trixie-20260713-slim - for the runner image
# E.g.: docker.io/debian:trixie-20260713-slim
#
# Find builder and runner images on Docker Hub or on Hex's Build Server (Bob).
# We recommend using Bob's Web UI to find recent tags:
@ -17,18 +17,19 @@
#
# For finding packages in Debian, search on https://packages.debian.org/.
ARG ELIXIR_VERSION=1.18.4
ARG OTP_VERSION=27.3.4.13
ARG DEBIAN_VERSION=trixie-20260623-slim
ARG ELIXIR_VERSION=1.20.2
ARG OTP_VERSION=29.0.3
ARG DEBIAN_VERSION=trixie-20260713-slim
ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}"
ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}@sha256:6fcd8ea864221b960c1ec418e3b10fa488298ff9e70c9e0f3db18070e610fb8a"
ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}@sha256:020c0d20b9880058cbe785a9db107156c3c75c2ac944a6aa7ab59f2add76a7bd"
FROM docker.io/node:22.21.1-bookworm-slim AS node_deps
FROM docker.io/node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d AS node_deps
WORKDIR /assets
COPY assets/package.json assets/package-lock.json ./
RUN npm ci
RUN npm install --global npm@12.0.1 \
&& npm ci
FROM ${BUILDER_IMAGE} AS builder

View File

@ -151,12 +151,22 @@ Open:
- Mailpit: <http://localhost:8028>
The script refuses to modify a pre-existing cluster named `who-need-help`
unless the project ownership marker exists.
unless the project ownership marker exists. On first install it creates
independent random application and PostgreSQL credentials in the
`who-need-help-local` Kubernetes Secret without writing them to Git. The local
PostGIS data directory persists inside the kind node. When migrating an older
project-owned cluster from the former `emptyDir` deployment, the script creates
and validates a local dump before replacing the database workload, then restores
that dump. After a successful rollout it also removes the obsolete chart Secret
and only the local Helm history revisions that stored the former inline
credential fields.
For an external cluster, provide a real PostgreSQL/PostGIS service and a
pre-created Secret through `existingSecret`. The chart intentionally has no
invented CPU/RAM limits or HPA thresholds; measure this application in the
target environment before setting them.
pre-created Secret through required `existingSecret`; the chart never renders
credentials from tracked values. The Secret must contain `DATABASE_URL`,
`SECRET_KEY_BASE`, `HANDOVER_SECRET`, and `RELEASE_COOKIE`. The chart
intentionally has no invented CPU/RAM limits or HPA thresholds; measure this
application in the target environment before setting them.
## Local Codex category review
@ -185,6 +195,7 @@ and no fallback provider. Recommendations require a human moderator action.
- [Architecture](docs/architecture.md)
- [Trust and safety](docs/trust-safety.md)
- [Implementation verification and known limits](docs/verification.md)
- [Verified dependency baseline](docs/dependency-baseline.md)
- [PostgreSQL/PostGIS ADR](docs/decisions/0001-postgresql-postgis-over-spacetimedb.md)
Exact dependency versions are locked in `mix.lock`,

View File

@ -2,8 +2,8 @@ FROM gradle:9.6.1-jdk17@sha256:7364ce528f33bb6038672bcef990d524f1ad8fbc292935819
USER root
ARG ANDROID_COMMAND_LINE_TOOLS_VERSION=14742923
ARG ANDROID_COMMAND_LINE_TOOLS_SHA1=48833c34b761c10cb20bcd16582129395d121b27
ARG ANDROID_COMMAND_LINE_TOOLS_VERSION=15859902
ARG ANDROID_COMMAND_LINE_TOOLS_SHA1=040d3996a65543d22ec4bf73e4c37aa37a8d4af4
ENV ANDROID_HOME=/opt/android-sdk
ENV ANDROID_SDK_ROOT=/opt/android-sdk
@ -21,12 +21,11 @@ RUN mkdir -p "${ANDROID_HOME}/cmdline-tools" \
| sha1sum -c - \
&& unzip -q /tmp/android-command-line-tools.zip -d /tmp/android-command-line-tools \
&& mv /tmp/android-command-line-tools/cmdline-tools "${ANDROID_HOME}/cmdline-tools/latest" \
&& rm -rf /tmp/android-command-line-tools /tmp/android-command-line-tools.zip \
&& yes | sdkmanager --licenses >/dev/null
&& rm -rf /tmp/android-command-line-tools /tmp/android-command-line-tools.zip
RUN sdkmanager \
"build-tools;37.0.0" \
"platforms;android-37.0" \
RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install \
"build-tools/37.0.0" \
"platforms/android-37.0" \
&& chown -R gradle:gradle "${ANDROID_HOME}"
FROM android-base AS android-sdk
@ -52,6 +51,11 @@ FROM android-base AS emulator
USER root
RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install \
"emulator" \
"system-images/android-37.0/google_apis_ps16k/x86_64" \
&& chown -R gradle:gradle "${ANDROID_HOME}"
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libdbus-1-3 \
@ -63,20 +67,16 @@ RUN apt-get update \
libxcomposite1 \
libxcursor1 \
libxi6 \
libxkbfile1 \
libxrandr2 \
libxrender1 \
libxtst6 \
&& rm -rf /var/lib/apt/lists/*
RUN sdkmanager \
"emulator" \
"system-images;android-37.0;google_apis_ps16k;x86_64" \
&& chown -R gradle:gradle "${ANDROID_HOME}"
# The container is operated from the host through `docker exec ... adb`.
# Keep platform-tools in the emulator target without invalidating the large
# system-image layer when application sources change.
RUN sdkmanager "platform-tools" \
RUN android --no-metrics --sdk="${ANDROID_HOME}" sdk install "platform-tools" \
&& chown -R gradle:gradle "${ANDROID_HOME}"
USER gradle

View File

@ -11,6 +11,8 @@ background-location permission.
- Android Gradle Plugin 9.3.0
- Gradle 9.6.1
- Android SDK Command-line Tools 22.0
- Android CLI 1.0.15857036 (embedded in the locked Command-line Tools archive)
- compileSdk / targetSdk 37
- Build Tools 37.0.0
- Java source and bytecode level 17

View File

@ -27,7 +27,7 @@ x-app-environment: &app-environment
services:
proxy:
image: traefik:v3.7
image: traefik:v3.7.8@sha256:4299bbed850421258fc5448c2e0e6ad350981d4d335a68de11b92448aedbefe5
command:
- --api.dashboard=false
- --providers.docker=true
@ -41,7 +41,7 @@ services:
restart: unless-stopped
db:
image: postgis/postgis:18-3.6-alpine
image: postgis/postgis:18-3.6-alpine@sha256:05d68c7f0f19b9aa0bf7c4a2049b2e8b38b44a63116392b95726a4c913766cf6
environment:
POSTGRES_DB: ${POSTGRES_DB:?Set POSTGRES_DB in .env}
POSTGRES_USER: ${POSTGRES_USER:?Set POSTGRES_USER in .env}
@ -57,7 +57,7 @@ services:
restart: unless-stopped
mailpit:
image: axllent/mailpit:v1.27
image: axllent/mailpit:v1.30.4@sha256:5a49a77c5bdbe7c5474450b4f46348d09949df3695257729c93a30369382d4f6
ports:
- "${MAILPIT_PORT:-8027}:8025"
networks: [edge, internal]

View File

@ -72,7 +72,7 @@ config :who_need_help, WhoNeedHelp.Mailer, adapter: Swoosh.Adapters.Local
# Configure esbuild (the version is required)
config :esbuild,
version: "0.25.4",
version: "0.28.1",
who_need_help: [
args:
~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
@ -82,7 +82,7 @@ config :esbuild,
# Configure tailwind (the version is required)
config :tailwind,
version: "4.3.0",
version: "4.3.3",
who_need_help: [
args: ~w(
--input=assets/css/app.css

View File

@ -22,5 +22,5 @@ app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{- define "who-need-help.secretName" -}}
{{- default (include "who-need-help.fullname" .) .Values.existingSecret }}
{{- required "existingSecret must name a pre-created Kubernetes Secret" .Values.existingSecret }}
{{- end }}

View File

@ -20,11 +20,10 @@ spec:
app.kubernetes.io/component: {{ $component }}
template:
metadata:
{{- with $root.Values.podAnnotations }}
annotations:
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") $root | sha256sum }}
{{- with $root.Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
labels:
{{- include "who-need-help.selectorLabels" $root | nindent 8 }}
app.kubernetes.io/component: {{ $component }}

View File

@ -1,18 +0,0 @@
{{- if not .Values.existingSecret }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "who-need-help.fullname" . }}
labels:
{{- include "who-need-help.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-install,pre-upgrade
helm.sh/hook-weight: "-10"
helm.sh/hook-delete-policy: before-hook-creation
type: Opaque
stringData:
DATABASE_URL: {{ .Values.app.databaseUrl | quote }}
SECRET_KEY_BASE: {{ .Values.app.secretKeyBase | quote }}
HANDOVER_SECRET: {{ .Values.app.handoverSecret | quote }}
RELEASE_COOKIE: {{ .Values.app.releaseCookie | quote }}
{{- end }}

View File

@ -8,11 +8,9 @@ service:
port: 80
nodePort: 30080
existingSecret: who-need-help-local
app:
host: localhost
scheme: http
urlPort: "4011"
databaseUrl: ecto://postgres:postgres@postgis/who_need_help
secretKeyBase: y9XxDIDxcn8uHarbX1B4feQl0xXi413tdYO472d758DtvslbtlVdUs2HdxPEPXCQ
handoverSecret: L2a4yz6nLYkWvIBTlREj8uDByngsY39JfLIfdYNDH/7dnIQEVW4T8OCjCkHOGZ7H
releaseCookie: OwNDSCX7aQkkNXovILLiESVolaRuYm67xC+JbMC3j3ERTY6sHIM/F4gc0elm1l07

View File

@ -20,10 +20,6 @@ app:
urlPort: "443"
port: "4000"
poolSize: "10"
databaseUrl: ecto://postgres:postgres@postgis/who_need_help
secretKeyBase: replace-before-public-deployment
handoverSecret: replace-before-public-deployment
releaseCookie: replace-before-public-deployment
codexSessionId: not-configured
# Shared limits are opt-in; set only after product policy thresholds are approved.
rateLimitPoliciesJson: "{}"
@ -31,6 +27,8 @@ app:
smtpRelay: mailpit
smtpPort: "1025"
# Required. The Secret must contain DATABASE_URL, SECRET_KEY_BASE,
# HANDOVER_SECRET, and RELEASE_COOKIE.
existingSecret: ""
ingress:

View File

@ -1,8 +1,9 @@
apiVersion: apps/v1
kind: Deployment
kind: StatefulSet
metadata:
name: postgis
spec:
serviceName: postgis
replicas: 1
selector:
matchLabels: {app: postgis}
@ -13,10 +14,23 @@ spec:
containers:
- name: postgis
image: postgis/postgis:18-3.6-alpine
imagePullPolicy: IfNotPresent
env:
- {name: POSTGRES_DB, value: who_need_help}
- {name: POSTGRES_USER, value: postgres}
- {name: POSTGRES_PASSWORD, value: postgres}
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: who-need-help-local
key: POSTGRES_DB
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: who-need-help-local
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: who-need-help-local
key: POSTGRES_PASSWORD
ports:
- {name: postgres, containerPort: 5432}
readinessProbe:
@ -27,7 +41,9 @@ spec:
- {name: data, mountPath: /var/lib/postgresql}
volumes:
- name: data
emptyDir: {}
hostPath:
path: /var/local/who-need-help/postgres
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
@ -52,7 +68,8 @@ spec:
spec:
containers:
- name: mailpit
image: axllent/mailpit:v1.27
image: axllent/mailpit:v1.30.4
imagePullPolicy: IfNotPresent
ports:
- {name: smtp, containerPort: 1025}
- {name: http, containerPort: 8025}

View File

@ -0,0 +1,95 @@
# Dependency baseline
Verified on 2026-07-18. This is a point-in-time stable baseline, not a claim
that future security updates or major-version migrations can be avoided.
Application locks and OCI digests remain committed so the same revision can be
rebuilt deterministically.
## Runtime and application
| Component | Locked version |
| --- | --- |
| Elixir | 1.20.2 |
| Erlang/OTP | 29.0.3 |
| Phoenix | 1.8.9 |
| Phoenix LiveView | 1.2.7 |
| Ecto / Ecto SQL | 3.14.1 / 3.14.0 |
| Oban | 2.23.0 |
| Node.js LTS | 24.18.0 |
| npm build tool | 12.0.1 |
| Tailwind CSS binary | 4.3.3 |
| esbuild binary | 0.28.1 |
| daisyUI | 5.6.18 |
| Heroicons | 2.2.0 |
| MapLibre GL JS | 5.24.0 |
`mix hex.outdated` reported every direct Hex dependency as up-to-date.
`npm outdated --json` returned an empty object, and the production `npm ci`
audit reported zero known vulnerabilities. The full transitive versions and
package checksums are in `mix.lock` and `assets/package-lock.json`.
## Services and container build
| Component | Locked version |
| --- | --- |
| PostgreSQL | 18.4 |
| PostGIS | 3.6.4 |
| Traefik | 3.7.8 |
| Mailpit | 1.30.4 |
| Debian builder/runner snapshot | trixie-20260713-slim |
Every external Compose/kind service image and every Dockerfile base image is
locked to both an exact tag and an OCI digest. The observed local Docker tooling
was Docker Engine/CLI 29.6.2, Compose 5.3.1, and Buildx 0.35.0; their official
release feeds reported those same versions as current during verification.
## Android and local Kubernetes
| Component | Locked version |
| --- | --- |
| Android Gradle Plugin | 9.3.0 |
| Gradle | 9.6.1 |
| Android SDK Command-line Tools | 22.0 |
| Android CLI | 1.0.15857036 |
| Android Emulator | 36.6.11.0 |
| compileSdk / targetSdk | 37 / 37 |
| Android Build Tools | 37.0.0 |
| AndroidX Activity | 1.13.0 |
| kubectl | 1.36.2 |
| kind | 0.32.0 |
| Helm | 4.2.3 |
Android 17 API 37 is the stable target used here. API 37.1 was not selected
because the official SDK channel identifies it as a QPR beta.
## Primary version sources
- [Elixir releases](https://github.com/elixir-lang/elixir/releases)
- [Erlang/OTP releases](https://github.com/erlang/otp/releases)
- [Hex package registry](https://hex.pm/)
- [Node.js release schedule](https://nodejs.org/en/about/previous-releases)
- [npm registry](https://www.npmjs.com/package/npm)
- [PostgreSQL 18.4 release notes](https://www.postgresql.org/docs/release/18.4/)
- [PostGIS release notes](https://postgis.net/docs/en/release_notes.html)
- [Traefik releases](https://github.com/traefik/traefik/releases)
- [Mailpit releases](https://github.com/axllent/mailpit/releases)
- [Android Gradle Plugin 9.3.0 release notes](https://developer.android.com/build/releases/agp-9-3-0-release-notes)
- [Android 17 SDK setup](https://developer.android.com/about/versions/17/setup-sdk)
- [Gradle release notes](https://docs.gradle.org/current/release-notes.html)
- [kubectl releases](https://dl.k8s.io/release/stable.txt)
- [kind releases](https://github.com/kubernetes-sigs/kind/releases)
- [Helm releases](https://github.com/helm/helm/releases)
- [Docker Engine releases](https://github.com/moby/moby/releases)
- [Docker Compose releases](https://github.com/docker/compose/releases)
- [Docker Buildx releases](https://github.com/docker/buildx/releases)
## Repeatable checks
```bash
docker run --rm who-need-help:test mix hex.outdated
docker build --target node_deps --tag who-need-help:node-deps .
docker run --rm who-need-help:node-deps npm --version
docker run --rm who-need-help:node-deps npm outdated --json
./scripts/test.sh
./scripts/android-build.sh
```

View File

@ -21,7 +21,8 @@ results from product limits and unknown production properties.
## Reproducible checks
- `./scripts/test.sh`: 124 tests, 0 failures in the final pre-upgrade run.
- `./scripts/test.sh`: 124 tests, 0 failures in the final post-upgrade run on
Elixir 1.20.2 and Erlang/OTP 29.0.3.
- `mix format --check-formatted`: passed in the final run.
- Android Docker build target: `testDebugUnitTest`, `lintDebug`, and
`assembleDebug` passed; the final lint report contains no errors or warnings.
@ -43,7 +44,7 @@ Local generated evidence (ignored by Git):
Android artifact:
- `android/dist/who-need-help-debug.apk`
- SHA-256 before the dependency-upgrade pass:
- Final post-upgrade SHA-256:
`063f3d8d877009ee229a403692e4b16517244dd9e7dddab4c2b3c202c5def4b8`
- Observed manifest values: version `0.1.0-debug`, minimum SDK 24, target and
compile SDK 37.
@ -65,6 +66,37 @@ excluded from the root Docker build context.
The final local Compose observation found 2 users, 1 help request, and 7
messages. These are local scenario data; the database is not assumed empty.
The final kind observation found 0 users, 0 help requests, and 0 messages. That
state was queried before changing the database workload. The migration from the
old `emptyDir` Deployment created a custom-format dump, validated it with
`pg_restore --list`, changed PostGIS to a persistent StatefulSet, and restored
the dump before the Helm upgrade. The final kind database reported PostgreSQL
18.4 and PostGIS 3.6.4.
The local kind Secret contains independently generated values. Only their
decoded lengths were inspected: the PostgreSQL password is 64 characters and
each application secret is 128 characters. Secret values were not printed or
written to tracked files.
## Dependency-upgrade observations
- The running Compose and kind releases reported Elixir 1.20.2 and Erlang/OTP
29.
- Compose reported Traefik 3.7.8 and Mailpit 1.30.4; Compose and kind both
reported PostgreSQL 18.4 and PostGIS 3.6.4.
- The production asset build reported Tailwind CSS 4.3.3 and daisyUI 5.6.18;
esbuild is configured at 0.28.1.
- The isolated Node build stage reported Node.js 24.18.0, npm 12.0.1,
`npm outdated --json` returned `{}`, and `npm ci` reported zero known
vulnerabilities.
- The Android build used Android Gradle Plugin 9.3.0, Gradle 9.6.1, Command-line
Tools 22.0, and its embedded Android CLI 1.0.15857036. Unit tests, lint, and
debug assembly passed; the lint report says “No errors or warnings.” The
rebuilt emulator image contains the API 37 ps16k AVD and APK, and its emulator
36.6.11.0 binary returned its version successfully at runtime.
- The final complete `scripts/kind-up.sh` run was idempotent and finished with
both 2-replica Deployments Ready and a successful cross-replica PubSub probe.
The migration `20260718114233` and reverse block lookup index
`blocks_blocked_id_blocker_id_index` were present. An actual `EXPLAIN ANALYZE`
for that reverse lookup selected the index. The table contained no block rows,

View File

@ -5,7 +5,7 @@ defmodule WhoNeedHelp.MixProject do
[
app: :who_need_help,
version: "0.1.0",
elixir: "~> 1.17",
elixir: "~> 1.20",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
@ -63,7 +63,7 @@ defmodule WhoNeedHelp.MixProject do
depth: 1},
{:daisyui,
github: "saadeghi/daisyui",
tag: "v5.5.20",
tag: "v5.6.18",
sparse: "packages/bundle",
app: false,
compile: false,

View File

@ -3,7 +3,7 @@
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
"daisyui": {:git, "https://github.com/saadeghi/daisyui.git", "22ecff57f2c391b80a75617325748cf4d13fdf47", [tag: "v5.5.20", sparse: "packages/bundle", depth: 1]},
"daisyui": {:git, "https://github.com/saadeghi/daisyui.git", "374d63758f997317e92e93e547569a5f9ad11250", [tag: "v5.6.18", sparse: "packages/bundle", depth: 1]},
"db_connection": {:hex, :db_connection, "2.10.2", "ae391e803a5adff104da913c2fc1c0c14a37f8b10001dcef568796e1fb7bf95c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "510b14482330f1af6490a2fa0efd8d4f1435d1529b165647df22ac0f2df0fa93"},
"decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"},
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},

View File

@ -8,6 +8,61 @@ export PATH
CLUSTER=who-need-help
MARKER="$ROOT/.tools/${CLUSTER}.owned"
NAMESPACE=who-need-help
SECRET_NAME=who-need-help-local
POSTGIS_IMAGE=postgis/postgis:18-3.6-alpine
POSTGIS_SOURCE="${POSTGIS_IMAGE}@sha256:05d68c7f0f19b9aa0bf7c4a2049b2e8b38b44a63116392b95726a4c913766cf6"
MAILPIT_IMAGE=axllent/mailpit:v1.30.4
MAILPIT_SOURCE="${MAILPIT_IMAGE}@sha256:5a49a77c5bdbe7c5474450b4f46348d09949df3695257729c93a30369382d4f6"
kube() {
kubectl --context "kind-${CLUSTER}" "$@"
}
case "$(uname -m)" in
x86_64) IMAGE_ARCH=amd64 ;;
aarch64|arm64) IMAGE_ARCH=arm64 ;;
*) echo "Unsupported container architecture: $(uname -m)" >&2; exit 1 ;;
esac
load_pinned_image() {
source=$1
local_tag=$2
archive=$3
docker pull --platform "linux/${IMAGE_ARCH}" "$source"
image_id=$(docker image inspect "$source" --format '{{.Id}}')
docker tag "$image_id" "$local_tag"
docker image save --platform "linux/${IMAGE_ARCH}" --output "$archive" "$local_tag"
kind load image-archive "$archive" --name "$CLUSTER"
rm -f "$archive"
}
remove_legacy_inline_secrets() {
legacy_secret=who-need-help-who-need-help
# Older chart revisions rendered credentials from tracked Helm values. Once
# the new deployments are healthy, remove the obsolete Secret and only those
# release-history records that still contain the former inline secret keys.
kube --namespace "$NAMESPACE" delete secret "$legacy_secret" \
--ignore-not-found >/dev/null
for revision in $(
helm history who-need-help \
--kube-context "kind-${CLUSTER}" \
--namespace "$NAMESPACE" |
awk 'NR > 1 {print $1}'
); do
if helm get values who-need-help \
--revision "$revision" \
--kube-context "kind-${CLUSTER}" \
--namespace "$NAMESPACE" |
grep -Eq '^[[:space:]]*(databaseUrl|secretKeyBase|handoverSecret|releaseCookie):'; then
kube --namespace "$NAMESPACE" delete secret \
--selector="owner=helm,name=who-need-help,version=${revision}"
fi
done
}
if kind get clusters | grep -Fxq "$CLUSTER"; then
if [ ! -f "$MARKER" ]; then
@ -20,25 +75,80 @@ else
touch "$MARKER"
fi
load_pinned_image "$POSTGIS_SOURCE" "$POSTGIS_IMAGE" "$ROOT/.tools/postgis-kind.tar"
load_pinned_image "$MAILPIT_SOURCE" "$MAILPIT_IMAGE" "$ROOT/.tools/mailpit-kind.tar"
docker build --tag who-need-help:local "$ROOT"
kind load docker-image who-need-help:local --name "$CLUSTER"
kubectl create namespace who-need-help --dry-run=client --output=yaml | kubectl apply -f -
kubectl --namespace who-need-help apply -f "$ROOT/deploy/kind/dependencies.yaml"
kubectl --namespace who-need-help rollout status deployment/postgis
kubectl --namespace who-need-help rollout status deployment/mailpit
kube create namespace "$NAMESPACE" --dry-run=client --output=yaml | kube apply -f -
legacy_backup=
if kube --namespace "$NAMESPACE" get deployment postgis >/dev/null 2>&1; then
backup_dir="$ROOT/output/backups"
mkdir -p "$backup_dir"
umask 077
legacy_backup="$backup_dir/kind-who-need-help-$(date +%Y%m%d-%H%M%S).dump"
kube --namespace "$NAMESPACE" exec deployment/postgis -- \
pg_dump --username postgres --dbname who_need_help --format custom >"$legacy_backup"
test -s "$legacy_backup"
kube --namespace "$NAMESPACE" exec --stdin deployment/postgis -- \
pg_restore --list <"$legacy_backup" >/dev/null
echo "Validated legacy kind database backup: $legacy_backup"
fi
if ! kube --namespace "$NAMESPACE" get secret "$SECRET_NAME" >/dev/null 2>&1; then
if kube --namespace "$NAMESPACE" get statefulset postgis >/dev/null 2>&1; then
echo "Secret '$SECRET_NAME' is missing while the persistent PostGIS StatefulSet exists." >&2
echo "Refusing to generate credentials that would not match the existing database." >&2
exit 1
fi
postgres_password=$(openssl rand -hex 32)
secret_key_base=$(openssl rand -hex 64)
handover_secret=$(openssl rand -hex 64)
release_cookie=$(openssl rand -hex 64)
kube --namespace "$NAMESPACE" create secret generic "$SECRET_NAME" \
--from-literal=POSTGRES_DB=who_need_help \
--from-literal=POSTGRES_USER=postgres \
--from-literal="POSTGRES_PASSWORD=$postgres_password" \
--from-literal="DATABASE_URL=ecto://postgres:${postgres_password}@postgis/who_need_help" \
--from-literal="SECRET_KEY_BASE=$secret_key_base" \
--from-literal="HANDOVER_SECRET=$handover_secret" \
--from-literal="RELEASE_COOKIE=$release_cookie"
unset postgres_password secret_key_base handover_secret release_cookie
fi
if [ -n "$legacy_backup" ]; then
kube --namespace "$NAMESPACE" delete deployment postgis --wait=true
fi
kube --namespace "$NAMESPACE" apply -f "$ROOT/deploy/kind/dependencies.yaml"
kube --namespace "$NAMESPACE" rollout status statefulset/postgis
kube --namespace "$NAMESPACE" rollout status deployment/mailpit
if [ -n "$legacy_backup" ]; then
kube --namespace "$NAMESPACE" exec --stdin statefulset/postgis -- \
pg_restore --username postgres --dbname who_need_help \
--clean --if-exists --no-owner <"$legacy_backup"
echo "Restored legacy kind database backup."
fi
helm upgrade --install who-need-help "$ROOT/deploy/helm/who-need-help" \
--namespace who-need-help \
--kube-context "kind-${CLUSTER}" \
--namespace "$NAMESPACE" \
--values "$ROOT/deploy/helm/who-need-help/values-kind.yaml" \
--set-string app.codexSessionId="${CODEX_SESSION_ID:-${CODEX_THREAD_ID:-not-configured}}" \
--wait
kubectl --namespace who-need-help rollout restart \
kube --namespace "$NAMESPACE" rollout restart \
deployment/who-need-help-who-need-help-web \
deployment/who-need-help-who-need-help-worker
kubectl --namespace who-need-help rollout status deployment/who-need-help-who-need-help-web
kubectl --namespace who-need-help rollout status deployment/who-need-help-who-need-help-worker
kube --namespace "$NAMESPACE" rollout status deployment/who-need-help-who-need-help-web
kube --namespace "$NAMESPACE" rollout status deployment/who-need-help-who-need-help-worker
remove_legacy_inline_secrets
"$ROOT/scripts/verify-realtime-cluster.sh" kind
echo "Who Need Help: http://localhost:4011"

View File

@ -19,7 +19,10 @@ case "$MODE" in
TARGET=$(
kubectl --context kind-who-need-help --namespace who-need-help get pods \
-l app.kubernetes.io/component=web \
-o jsonpath='{.items[0].metadata.name}'
--field-selector=status.phase=Running \
--no-headers \
-o custom-columns='NAME:.metadata.name,READY:.status.containerStatuses[0].ready,DELETING:.metadata.deletionTimestamp' |
awk '$2 == "true" && $3 == "<none>" {print $1; exit}'
)
if [ -z "$TARGET" ]; then
echo "No running kind web replica was found." >&2