export PATH := if os() == 'windows' { env_var('PATH') } else { env_var('PATH') + ':' + env_var('HOME') + '/go/bin' } exe := if os() == 'windows' { '.exe' } else { '' } go_pkgs := './internal/... ./cmd/... .' # mise pins the linter; a stale golangci-lint earlier on PATH is built against an older Go and panics golangci := 'mise exec -- golangci-lint' addr := env_var_or_default('SPINOZA_ADDR', '127.0.0.1:34115') test_cluster := env_var_or_default('SPINOZA_KIND_CLUSTER', 'spinoza') test_context := 'kind-' + test_cluster kind_dir := 'test/integration' kind_merged := '.tmp/kind' registry_host := 'localhost:5001' argocd_chart := '10.9.6' registry_endpoint := 'http://kind-registry:5000' ldflags := '-s -w' version_pkg := 'github.com/sophotechlabs/spinoza/internal/version.value' default: @just --list [private] app-version: #!/usr/bin/env bash set -euo pipefail if [ -n "${SPINOZA_VERSION:-}" ]; then echo "${SPINOZA_VERSION}" exit 0 fi if [ -n "${GITHUB_REF_NAME:-}" ]; then echo "${GITHUB_REF_NAME}" exit 0 fi described=$(git describe --tags --always --dirty 2>/dev/null || true) if [ -n "$described" ]; then echo "$described" exit 0 fi echo dev deps: cd frontend && npm ci tidy: go mod tidy build flags='': #!/usr/bin/env bash set -euo pipefail export SPINOZA_VERSION="$(just app-version)" cd frontend && npm run build && cd .. go build {{ flags }} -trimpath -ldflags "{{ ldflags }} -X {{ version_pkg }}=$SPINOZA_VERSION" -o spinoza{{ exe }} . build-e2e: (build '-cover -covermode=atomic') stub-assets: #!/usr/bin/env bash set -euo pipefail if [ -f web/dist/index.html ]; then exit 0 fi mkdir -p web/dist echo "stub-assets: writing a placeholder index.html so the go-only recipes can compile" cat > web/dist/index.html <<'HTML' Spinoza This binary was built without its frontend. Run just build. HTML run: build stop ./spinoza{{ exe }} stop: #!/usr/bin/env bash set -euo pipefail listen='{{ addr }}' port="${listen##*:}" if ! command -v lsof > /dev/null; then echo "stop: lsof is missing, so nothing was checked on port $port" exit 0 fi holders=$(lsof -ti "tcp:${port}" -sTCP:LISTEN 2>/dev/null || true) if [ -z "$holders" ]; then exit 0 fi for pid in $holders; do command=$(ps -p "$pid" -o args= 2>/dev/null || true) case "$command" in *spinoza*) echo "stop: port $port was held by pid $pid, stopping it" kill "$pid" 2>/dev/null || true ;; *) echo "stop: port $port is held by pid $pid, which is not spinoza:" echo " $command" exit 1 ;; esac done for _ in $(seq 1 50); do if [ -z "$(lsof -ti "tcp:${port}" -sTCP:LISTEN 2>/dev/null || true)" ]; then exit 0 fi sleep 0.1 done echo "stop: port $port is still held after 5s" exit 1 build-desktop: #!/usr/bin/env bash set -euo pipefail version=$(just app-version) export SPINOZA_VERSION="$version" numeric="" if printf '%s' "$version" | grep -Eq '^v[0-9]+(\.[0-9]+){0,2}([-+].*)?$'; then numeric="${version#v}" numeric="${numeric%%[-+]*}" fi backup=$(mktemp) cp wails.json "$backup" trap 'mv "$backup" wails.json' EXIT if [ -n "$numeric" ]; then export PRODUCT_VERSION="$numeric" yq -i -o=json '.info.productVersion = strenv(PRODUCT_VERSION)' wails.json fi wails build -platform darwin/universal -tags desktop -skipbindings -trimpath -ldflags "{{ ldflags }} -X {{ version_pkg }}=$version" plist=build/bin/spinoza.app/Contents/Info.plist if [ -f "$plist" ]; then plutil -lint "$plist" fi just icns build-desktop-windows arch='amd64': #!/usr/bin/env bash set -euo pipefail version=$(just app-version) export SPINOZA_VERSION="$version" numeric="" if printf '%s' "$version" | grep -Eq '^v[0-9]+(\.[0-9]+){0,2}([-+].*)?$'; then numeric="${version#v}" numeric="${numeric%%[-+]*}" fi backup=$(mktemp) cp wails.json "$backup" trap 'mv "$backup" wails.json' EXIT if [ -n "$numeric" ]; then export PRODUCT_VERSION="$numeric" yq -i -o=json '.info.productVersion = strenv(PRODUCT_VERSION)' wails.json fi wails build -platform windows/{{ arch }} -tags desktop -skipbindings -trimpath -ldflags "{{ ldflags }} -X {{ version_pkg }}=$version" package-desktop-windows arch='amd64': (build-desktop-windows arch) #!/usr/bin/env bash set -euo pipefail version=$(just app-version) built=build/bin/Spinoza.exe if [ ! -f "$built" ]; then echo "package-desktop-windows: $built was not built" exit 1 fi staged=$(mktemp -d) trap 'rm -rf "$staged"' EXIT cp "$built" "$staged/Spinoza.exe" cp LICENSE "$staged/LICENSE" mkdir -p dist/release archive="$PWD/dist/release/spinoza_${version}_windows_{{ arch }}_app.zip" rm -f "$archive" (cd "$staged" && zip -q -X "$archive" Spinoza.exe LICENSE) echo "package-desktop-windows: wrote $archive" build-desktop-linux arch='amd64': #!/usr/bin/env bash set -euo pipefail version=$(just app-version) export SPINOZA_VERSION="$version" wails build -platform linux/{{ arch }} -tags desktop,webkit2_41 -skipbindings -trimpath -ldflags "{{ ldflags }} -X {{ version_pkg }}=$version" package-desktop-linux arch='amd64': (build-desktop-linux arch) #!/usr/bin/env bash set -euo pipefail version=$(just app-version) built=build/bin/Spinoza if [ ! -f "$built" ]; then echo "package-desktop-linux: $built was not built" exit 1 fi staged=$(mktemp -d) trap 'rm -rf "$staged"' EXIT cp "$built" "$staged/Spinoza" cp build/appicon.png "$staged/spinoza.png" cp LICENSE "$staged/LICENSE" mkdir -p dist/release archive="$PWD/dist/release/spinoza_${version}_linux_{{ arch }}_app.tar.gz" rm -f "$archive" reported=$("$built" --version) if [ "$reported" != "$version" ]; then echo "package-desktop-linux: the app reports $reported, not $version" exit 1 fi tar --sort=name --owner=0 --group=0 --numeric-owner --mtime=@0 -cf - -C "$staged" Spinoza spinoza.png LICENSE | gzip -n > "$archive" echo "package-desktop-linux: wrote $archive, built from an app that reports $reported" icns: #!/usr/bin/env bash set -euo pipefail bundle=build/bin/spinoza.app if [ ! -d "$bundle" ]; then echo "icns: $bundle is not built yet" exit 1 fi work=$(mktemp -d) trap 'rm -rf "$work"' EXIT iconset="$work/spinoza.iconset" mkdir -p "$iconset" for size in 16 32 128 256 512; do sips -z "$size" "$size" build/appicon.png --out "$iconset/icon_${size}x${size}.png" >/dev/null retina=$((size * 2)) sips -z "$retina" "$retina" build/appicon.png --out "$iconset/icon_${size}x${size}@2x.png" >/dev/null done iconutil -c icns "$iconset" -o "$bundle/Contents/Resources/iconfile.icns" touch "$bundle" rund: build-desktop open build/bin/spinoza.app dev-desktop: wails dev -tags desktop -skipbindings dev-api: stub-assets go run . --addr 127.0.0.1:34115 dev-web: cd frontend && npm run dev test-be name='' pkgs=go_pkgs: stub-assets #!/usr/bin/env bash set -euo pipefail name={{ quote(name) }} if [ -n "$name" ]; then go test -race -shuffle=on -run "$name" {{ pkgs }} exit 0 fi go test -race -shuffle=on -covermode=atomic -coverprofile=coverage.out {{ pkgs }} go tool cover -func=coverage.out bench-be name pkgs=go_pkgs: stub-assets go test -run '^$' -bench {{ quote(name) }} -benchmem -count 5 -timeout 60m {{ pkgs }} kind-config tier: #!/usr/bin/env bash set -euo pipefail case '{{ tier }}' in base) chain=('{{ kind_dir }}/kind.yaml') ;; e2e) chain=('{{ kind_dir }}/kind.yaml' '{{ kind_dir }}/kind-e2e.yaml') ;; full) chain=('{{ kind_dir }}/kind.yaml' '{{ kind_dir }}/kind-e2e.yaml' '{{ kind_dir }}/kind-full.yaml') ;; *) echo "kind-config: {{ tier }} is not one of base, e2e, full" exit 1 ;; esac mkdir -p {{ kind_merged }} yq eval-all '. as $item ireduce ({}; . *+ $item)' "${chain[@]}" > {{ kind_merged }}/{{ tier }}.yaml echo "kind-config: {{ kind_merged }}/{{ tier }}.yaml is ${chain[*]} merged, $(yq '.nodes | length' {{ kind_merged }}/{{ tier }}.yaml) nodes" [private] cluster-up tier: #!/usr/bin/env bash set -euo pipefail just kind-config {{ tier }} config={{ kind_merged }}/{{ tier }}.yaml if ! kind get clusters | grep -qx {{ test_cluster }}; then scripts/create-kind-cluster.sh {{ test_cluster }} --config "$config" --wait 300s fi kind export kubeconfig --name {{ test_cluster }} wanted=$(yq '.nodes | length' "$config") running=$(kind get nodes --name {{ test_cluster }} | wc -l | tr -d ' ') if [ "$running" != "$wanted" ]; then echo "cluster {{ test_cluster }} runs $running nodes, not the $wanted in $config; just cluster-down first" exit 1 fi just cluster-mirror kubectl --context {{ test_context }} cluster-info kubectl --context {{ test_context }} get nodes -L spinoza.test/pool images=("$(yq '.spec.template.spec.containers[] | select(.name == "metrics-server") | .image' {{ kind_dir }}/metrics-server.yaml)") if [ '{{ tier }}' != base ]; then images+=(busybox:1.37 busybox@sha256:9db7b59979c38555a39def84a31fb98b5296952f9e3afd4f6f11f05b07adfab0 busybox:latest registry.k8s.io/pause:3.10) fi just kind-sideload "${images[@]}" kubectl --context {{ test_context }} apply -f {{ kind_dir }}/metrics-server.yaml kubectl --context {{ test_context }} -n kube-system rollout status deployment/metrics-server --timeout=5m kubectl --context {{ test_context }} wait --for=condition=Available apiservice/v1beta1.metrics.k8s.io --timeout=5m [private] kind-sideload +images: #!/usr/bin/env bash set -euo pipefail pull_image() { local image="$1" local attempt for attempt in 1 2 3; do if docker pull "$image"; then return 0 fi if [ "$attempt" -lt 3 ]; then sleep $((attempt * 5)) fi done return 1 } for image in {{ images }}; do if ! docker image inspect "$image" > /dev/null 2>&1; then pull_image "$image" fi for node in $(kind get nodes --name {{ test_cluster }}); do if ! docker exec "$node" crictl inspecti "$image" > /dev/null 2>&1; then docker image save "$image" | docker exec -i "$node" ctr --namespace=k8s.io images import --digests --snapshotter=overlayfs - fi done done cluster-base: (cluster-up 'base') cluster-e2e: (cluster-up 'e2e') cluster-full: (cluster-up 'full') cluster-gitops cluster-second [private] cluster-second: #!/usr/bin/env bash set -euo pipefail if ! kind get clusters | grep -qx {{ test_cluster }}-second; then scripts/create-kind-cluster.sh {{ test_cluster }}-second --wait 300s fi kubectl --context kind-{{ test_cluster }}-second cluster-info [private] cluster-gitops: #!/usr/bin/env bash set -euo pipefail helm --kube-context {{ test_context }} repo add fluxcd-community https://fluxcd-community.github.io/helm-charts --force-update helm --kube-context {{ test_context }} repo add argo https://argoproj.github.io/argo-helm --force-update helm --kube-context {{ test_context }} repo update helm --kube-context {{ test_context }} upgrade --install flux fluxcd-community/flux2 \ --namespace flux-system --create-namespace --wait --timeout 10m argocd_values=(--set dex.enabled=false --set notifications.enabled=false --set applicationSet.enabled=true --set redis.image.repository=mirror.gcr.io/library/redis) argocd_images=$(helm template argocd argo/argo-cd --version {{ argocd_chart }} --namespace argocd "${argocd_values[@]}" | yq -N '.. | select(tag == "!!map") | select(has("image")) | .image' | sort -u) just kind-sideload $argocd_images helm --kube-context {{ test_context }} upgrade --install argocd argo/argo-cd --version {{ argocd_chart }} \ --namespace argocd --create-namespace --wait --timeout 10m "${argocd_values[@]}" kubectl --context {{ test_context }} -n flux-system wait --for=condition=Available deployment --all --timeout=10m kubectl --context {{ test_context }} -n argocd wait --for=condition=Available deployment --all --timeout=10m cluster-down: #!/usr/bin/env bash set -euo pipefail kind delete cluster --name {{ test_cluster }} if kind get clusters | grep -qx {{ test_cluster }}-second; then kind delete cluster --name {{ test_cluster }}-second fi [private] cluster-mirror: #!/usr/bin/env bash set -euo pipefail certs="/etc/containerd/certs.d/{{ registry_host }}" for node in $(kind get nodes --name {{ test_cluster }}); do docker exec "$node" mkdir -p "$certs" printf '[host."%s"]\n' '{{ registry_endpoint }}' | docker exec -i "$node" cp /dev/stdin "$certs/hosts.toml" done test-integration name='': cluster-base #!/usr/bin/env bash set -euo pipefail export SPINOZA_TEST_CONTEXT={{ test_context }} name={{ quote(name) }} if [ "$name" = MCP ]; then output=.tmp/system-coverage/mcp-cli mkdir -p "$output" status=0 if ! go test -tags integration -count=1 -timeout 15m -v -run MCP \ -covermode=atomic -coverprofile="$output/coverage.out" \ -coverpkg=./internal/... ./test/integration/...; then status=1 fi node scripts/system-coverage.mjs "$output/coverage.out" mcp-cli "$output/summary.json" exit "$status" fi if [ -n "$name" ]; then go test -tags integration -count=1 -timeout 15m -run "$name" ./test/integration/... exit 0 fi go test -tags integration -count=1 -timeout 15m -covermode=atomic -coverprofile=coverage.integration.out -coverpkg=./internal/... ./test/integration/... go tool cover -func=coverage.integration.out | tail -1 test-traffic-live kubeconfig='': #!/usr/bin/env bash set -euo pipefail kubeconfig={{ quote(kubeconfig) }} if [ -n "$kubeconfig" ]; then export KUBECONFIG="$kubeconfig" fi SPINOZA_LIVE_TRAFFIC=1 go test -v -count=1 -timeout 1m ./test/live test-e2e name='' spec='': cluster-e2e (e2e-run 'core' name spec) shots name='' spec='': cluster-down cluster-e2e cluster-second #!/usr/bin/env bash set -euo pipefail SPINOZA_E2E_TIER=shots just e2e-run shots {{ quote(name) }} {{ quote(spec) }} test-e2e-full name='' spec='': cluster-full #!/usr/bin/env bash set -euo pipefail SPINOZA_E2E_TIER=full just e2e-run full {{ quote(name) }} {{ quote(spec) }} test-e2e-group group browser='chromium' name='' spec='': #!/usr/bin/env bash set -euo pipefail group={{ quote(group) }} browser={{ quote(browser) }} profile=$(node e2e/scripts/group-field.mjs "$group" profile) runner=$(node e2e/scripts/group-field.mjs "$group" runner) if [ "$runner" != playwright ]; then echo "test-e2e-group: $group uses $runner" exit 1 fi case "$profile" in core|full) ;; *) echo "test-e2e-group: $group has unsupported Playwright profile $profile" exit 1 ;; esac export SPINOZA_E2E_GROUP="$group" export SPINOZA_E2E_BROWSER="$browser" export SPINOZA_E2E_TIER="$profile" just e2e-run "$browser" {{ quote(name) }} {{ quote(spec) }} validate-e2e-suite: node e2e/scripts/validate-suite.mjs test-e2e-scripts: node --test 'e2e/scripts/*.test.mjs' 'scripts/*.test.mjs' test-scripts: go test ./scripts/... -count=1 e2e-cover dir='e2e/.tmp/cover': scripts/e2e-cover.sh {{ quote(dir) }} e2e/coverage.out e2e-cover-merge dir expected selected total: scripts/e2e-cover.sh {{ quote(dir) }} e2e/coverage.out {{ quote(expected) }} {{ quote(selected) }} {{ quote(total) }} e2e-flaky report='e2e/test-results/report.json': node e2e/scripts/flaky.mjs {{ quote(report) }} [private] e2e-run project name='' spec='': #!/usr/bin/env bash set -euo pipefail export SPINOZA_KIND_CLUSTER='{{ test_cluster }}' if [ ! -d frontend/node_modules ] || [ frontend/package-lock.json -nt frontend/node_modules ]; then npm --prefix frontend ci fi cd e2e if [ ! -d node_modules ] || [ package-lock.json -nt node_modules ]; then npm ci fi browser="{{ project }}" case "$browser" in core|full|shots) browser=chromium ;; esac install_with_deps() { local attempt for attempt in 1 2 3; do if npx playwright install --with-deps "$browser"; then return 0 fi if [ "$attempt" -lt 3 ]; then sleep $((attempt * 5)) fi done return 1 } install_deps=no if [ -n "${CI:-}" ]; then if command -v sudo > /dev/null 2>&1; then if sudo -n true > /dev/null 2>&1; then install_deps=yes fi fi fi if [ "$install_deps" = yes ]; then install_with_deps else npx playwright install "$browser" fi run=(test --project={{ project }}) spec={{ quote(spec) }} if [ -n "$spec" ]; then run+=("$spec") fi name={{ quote(name) }} if [ -n "$name" ]; then run+=(--grep "$name") fi npx playwright "${run[@]}" test-fe name='' spec='': #!/usr/bin/env bash set -euo pipefail if [ ! -d frontend/node_modules ] || [ frontend/package-lock.json -nt frontend/node_modules ]; then npm --prefix frontend ci fi cd frontend name={{ quote(name) }} spec={{ quote(spec) }} if [ -z "$name" ] && [ -z "$spec" ]; then npm run test:coverage exit 0 fi run=(run) if [ -n "$spec" ]; then run+=("$spec") fi if [ -n "$name" ]; then run+=(-t "$name") fi npx vitest "${run[@]}" test: test-be test-fe test-repeat name='' pkgs=go_pkgs: stub-assets #!/usr/bin/env bash set -euo pipefail name={{ quote(name) }} if [ -n "$name" ]; then go test -race -shuffle=on -count=5 -run "$name" {{ pkgs }} exit 0 fi go test -race -shuffle=on -count=5 {{ pkgs }} fuzz-one pkg target tags='' duration='30s': stub-assets #!/usr/bin/env bash set -euo pipefail args=(test {{ quote(pkg) }} -run '^$' -fuzz {{ quote(target) }} -fuzztime {{ quote(duration) }} -timeout 2m) tags={{ quote(tags) }} if [ -n "$tags" ]; then args+=(-tags "$tags") fi go "${args[@]}" mutation mode='default' output='dist/mutation/default.json': stub-assets #!/usr/bin/env bash set -euo pipefail mode={{ quote(mode) }} output={{ quote(output) }} mkdir -p "$(dirname "$output")" cached_build_timeout_coefficient=60 args=(unleash --timeout-coefficient "$cached_build_timeout_coefficient" --output "$output" --output-statuses lc) if [ "$mode" = desktop ]; then args+=(--tags desktop) elif [ "$mode" != default ]; then echo "mutation: mode must be default or desktop" >&2 exit 1 fi gremlins "${args[@]}" scripts/check-mutation-report.sh "$output" [private] mutation-package path tags output exclude='': stub-assets #!/usr/bin/env bash set -euo pipefail path={{ quote(path) }} tags={{ quote(tags) }} output={{ quote(output) }} exclude={{ quote(exclude) }} mkdir -p "$(dirname "$output")" cached_build_timeout_coefficient=60 args=(unleash "$path" --timeout-coefficient "$cached_build_timeout_coefficient" --output "$output" --output-statuses lc) if [ -n "$tags" ]; then args+=(--tags "$tags") fi if [ -n "$exclude" ]; then args+=(--exclude-files "$exclude") fi echo "mutation: testing $path" gremlins "${args[@]}" if [ ! -f "$output" ]; then echo "mutation: $path has no supported mutants" printf '%s\n' '{"test_efficacy":0,"mutants_killed":0,"mutants_lived":0,"mutants_not_covered":0,"files":[]}' > "$output" exit 0 fi scripts/check-mutation-report.sh "$output" mutation-shard shard output='dist/mutation': stub-assets #!/usr/bin/env bash set -euo pipefail shard={{ quote(shard) }} output={{ quote(output) }} tags= exclude= paths=() case "$shard" in root-default) paths=(.) exclude='^(cmd|internal|scripts|test)/' ;; root-desktop) paths=(.) tags=desktop exclude='^(cmd|internal|scripts|test)/' ;; cmd) for path in cmd/*; do if [ -d "$path" ]; then paths+=("$path") fi done ;; internal-a-d) for path in internal/[a-d]*; do if [ ! -d "$path" ]; then continue fi if [ "$path" = internal/checks ]; then continue fi paths+=("$path") done ;; checks-a-e) paths=(internal/checks) exclude='^[f-z]' ;; checks-f-j) paths=(internal/checks) exclude='^([a-e]|[k-z])' ;; checks-k-o) paths=(internal/checks) exclude='^([a-j]|[p-z])' ;; checks-p-r) paths=(internal/checks) exclude='^([a-o]|[s-z])' ;; checks-s-t) paths=(internal/checks) exclude='^([a-r]|[u-z])' ;; checks-u-z) paths=(internal/checks) exclude='^[a-t]' ;; internal-e-l) for path in internal/[e-l]*; do if [ -d "$path" ]; then paths+=("$path") fi done ;; internal-m-r) for path in internal/[m-r]*; do if [ ! -d "$path" ]; then continue fi if [ "$path" = internal/resources ]; then continue fi paths+=("$path") done ;; resources-a-f) paths=(internal/resources) exclude='^[g-z]' ;; resources-g-l) paths=(internal/resources) exclude='^([a-f]|[m-z])' ;; resources-m-r) paths=(internal/resources) exclude='^([a-l]|[s-z])' ;; resources-s-z) paths=(internal/resources) exclude='^[a-r]' ;; server-a-c) paths=(internal/server) exclude='^[d-z]' ;; server-d-e) paths=(internal/server) exclude='^([a-c]|[f-z])' ;; server-f) paths=(internal/server) exclude='^([a-e]|[g-z])' ;; server-g-l) paths=(internal/server) exclude='^([a-f]|[m-z])' ;; server-m-r) paths=(internal/server) exclude='^([a-l]|[s-z])' ;; server-s-z) paths=(internal/server) exclude='^[a-r]' ;; internal-s-z) for path in internal/[s-z]*; do if [ ! -d "$path" ]; then continue fi if [ "$path" = internal/server ]; then continue fi paths+=("$path") done ;; *) echo "mutation: unknown shard $shard" >&2 exit 1 ;; esac if [ "${#paths[@]}" -eq 0 ]; then echo "mutation: shard $shard has no packages" >&2 exit 1 fi for path in "${paths[@]}"; do name=${path#./} if [ "$name" = . ]; then name=root fi name=${name//\//-} just mutation-package "$path" "$tags" "$output/$shard-$name.json" "$exclude" done mutation-total reports='dist/mutation': scripts/check-mutation-total.sh {{ quote(reports) }} cover-gate: test-be go-test-coverage --config .testcoverage.yml badges: cover-gate test-fe #!/usr/bin/env bash set -euo pipefail color() { if [ "$1" -ge 90 ]; then echo '#97ca00' return fi if [ "$1" -ge 75 ]; then echo '#dfb317' return fi echo '#e05d44' } endpoint() { printf '{"schemaVersion":1,"label":"%s","message":"%s%%","color":"%s"}\n' "$2" "$3" "$(color "$3")" > "dist/badges/$1" } mkdir -p dist/badges out=$(mktemp) trap 'rm -f "$out"' EXIT GITHUB_OUTPUT="$out" go-test-coverage --config .testcoverage.yml -o > /dev/null go_pct=$(grep '^total-coverage=' "$out" | cut -d = -f 2) web_pct=$(node -p "Math.floor(require('./frontend/coverage/coverage-summary.json').total.lines.pct)") endpoint coverage-go.json 'go coverage' "$go_pct" endpoint coverage-web.json 'web coverage' "$web_pct" echo "badges: go ${go_pct}%, web ${web_pct}%" publish-badges: #!/usr/bin/env bash set -euo pipefail if [ ! -f dist/badges/coverage-go.json ] || [ ! -f dist/badges/coverage-web.json ]; then echo "publish-badges: dist/badges is empty, run just badges first" exit 1 fi if [ -z "${GITHUB_REPOSITORY:-}" ]; then echo "publish-badges: GITHUB_REPOSITORY is unset" exit 1 fi if [ -z "${GH_TOKEN:-}" ]; then echo "publish-badges: GH_TOKEN is unset" exit 1 fi work=$(mktemp -d) trap 'rm -rf "$work"' EXIT cp dist/badges/coverage-go.json dist/badges/coverage-web.json "$work/" git -C "$work" init -q -b badges git -C "$work" add coverage-go.json coverage-web.json git -C "$work" -c user.name='github-actions[bot]' -c user.email='41898282+github-actions[bot]@users.noreply.github.com' commit -q -m 'chore(badges): publish coverage' git -C "$work" push -q --force "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" badges:badges lint-be: stub-assets {{ golangci }} run {{ go_pkgs }} {{ golangci }} run --build-tags desktop {{ go_pkgs }} {{ golangci }} run --build-tags integration ./test/... go vet {{ go_pkgs }} go vet -tags desktop {{ go_pkgs }} go vet -tags integration ./test/... lint-fe: cd frontend && npm run lint cd frontend && npm run typecheck cd frontend && npm run format:check cd frontend && npm run contrast lint-e2e: #!/usr/bin/env bash set -euo pipefail cd e2e if [ ! -d node_modules ] || [ package-lock.json -nt node_modules ]; then npm ci fi npm run typecheck cm_dir := 'test/clustermode' cm_cluster := env_var_or_default('SPINOZA_CM_CLUSTER', test_cluster + '-cm') cm_context := 'kind-' + cm_cluster # stand up a cluster with an ingress, keycloak and the fixtures cluster mode is verified against cluster-mode-up: #!/usr/bin/env bash set -euo pipefail if ! kind get clusters | grep -qx {{ cm_cluster }}; then scripts/create-kind-cluster.sh {{ cm_cluster }} --config {{ cm_dir }}/kind.yaml --wait 300s fi kind export kubeconfig --name {{ cm_cluster }} kubectl --context {{ cm_context }} apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.13.1/deploy/static/provider/kind/deploy.yaml kubectl --context {{ cm_context }} -n ingress-nginx patch deployment ingress-nginx-controller --type=merge \ -p '{"spec":{"template":{"spec":{"nodeSelector":{"ingress-ready":"true"}}}}}' kubectl --context {{ cm_context }} -n ingress-nginx rollout status deployment/ingress-nginx-controller --timeout=5m kubectl --context {{ cm_context }} -n ingress-nginx wait --for=condition=ready pod \ --selector=app.kubernetes.io/component=controller --timeout=5m for _ in $(seq 1 60); do if kubectl --context {{ cm_context }} -n ingress-nginx get endpoints ingress-nginx-controller-admission \ -o jsonpath='{.subsets[0].addresses[0].ip}' 2>/dev/null | grep -q .; then break fi sleep 2 done mkdir -p .tmp/cm if [ ! -f .tmp/cm/tls.crt ]; then openssl req -x509 -newkey rsa:2048 -nodes -days 30 \ -keyout .tmp/cm/tls.key -out .tmp/cm/tls.crt \ -subj "/CN=localtest.me" \ -addext "subjectAltName=DNS:localtest.me,DNS:spinoza.localtest.me,DNS:keycloak.localtest.me" fi kubectl --context {{ cm_context }} apply -f {{ cm_dir }}/rbac.yaml kubectl --context {{ cm_context }} create namespace spinoza --dry-run=client -o yaml | kubectl --context {{ cm_context }} apply -f - for ns in keycloak spinoza; do kubectl --context {{ cm_context }} create namespace "$ns" --dry-run=client -o yaml | kubectl --context {{ cm_context }} apply -f - kubectl --context {{ cm_context }} -n "$ns" create secret tls localtest-tls \ --cert=.tmp/cm/tls.crt --key=.tmp/cm/tls.key --dry-run=client -o yaml \ | kubectl --context {{ cm_context }} apply -f - done kubectl --context {{ cm_context }} -n keycloak create configmap spinoza-realm \ --from-file=realm.json={{ cm_dir }}/realm.json --dry-run=client -o yaml \ | kubectl --context {{ cm_context }} apply -f - admitted() { local file="$1" local attempt local out for attempt in $(seq 1 30); do if out=$(kubectl --context {{ cm_context }} apply -f "$file" 2>&1); then printf '%s\n' "$out" return 0 fi if ! grep -q 'validate.nginx.ingress.kubernetes.io' <<< "$out"; then printf '%s\n' "$out" >&2 return 1 fi sleep 2 done printf '%s\n' "$out" >&2 return 1 } admitted {{ cm_dir }}/keycloak.yaml kubectl --context {{ cm_context }} apply -f {{ cm_dir }}/shim.yaml kubectl --context {{ cm_context }} apply -f {{ cm_dir }}/workloads.yaml realm=$(shasum -a 256 {{ cm_dir }}/realm.json | cut -d' ' -f1) kubectl --context {{ cm_context }} -n keycloak patch deployment/keycloak --type=merge \ -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"spinoza.test/realm\":\"$realm\"}}}}}" kubectl --context {{ cm_context }} -n keycloak rollout status deployment/keycloak --timeout=8m kubectl --context {{ cm_context }} -n keycloak rollout status deployment/kcshim --timeout=5m kubectl --context {{ cm_context }} -n payments rollout status deployment/web --timeout=5m kubectl --context {{ cm_context }} -n default rollout status deployment/other --timeout=5m cluster-mode-image: #!/usr/bin/env bash set -euo pipefail version="$(just app-version)" named="${KIND_EXPERIMENTAL_DOCKER_NETWORK:-}" if [ -z "$named" ]; then docker build --build-arg SPINOZA_GOFLAGS="-cover -covermode=atomic" --build-arg SPINOZA_VERSION="$version" -t spinoza:cluster-mode . else # buildkit takes a network from its builder, not from the build, and the # default bridge is the one this host's firewall will not forward. builder="spinoza-$named" if ! docker buildx inspect "$builder" > /dev/null 2>&1; then docker buildx create --name "$builder" --driver docker-container \ --driver-opt "network=$named" > /dev/null fi docker buildx build --builder "$builder" --load \ --build-arg SPINOZA_GOFLAGS="-cover -covermode=atomic" \ --build-arg SPINOZA_VERSION="$version" -t spinoza:cluster-mode . fi kind load docker-image spinoza:cluster-mode --name {{ cm_cluster }} cluster-mode-cover-prepare: bash scripts/cluster-mode-coverage.sh prepare {{ cm_context }} cluster-mode-cover suite: bash scripts/cluster-mode-coverage.sh collect {{ cm_context }} {{ quote(suite) }} cluster-mode-down: kind delete cluster --name {{ cm_cluster }} # every cluster-mode path, against a real cluster and a real identity provider test-cluster-mode name='': stub-assets cluster-mode-up cluster-mode-image cluster-mode-cover-prepare #!/usr/bin/env bash set -euo pipefail export SPINOZA_CM_COVERAGE=1 finish() { status=$? trap - EXIT if ! just cluster-mode-cover cluster-mode-auth; then exit 1 fi exit "$status" } trap finish EXIT args=(-tags clustermode -count=1 -timeout 45m -v) if [ -n '{{ name }}' ]; then args+=(-run '{{ name }}') fi SPINOZA_CM_CONTEXT={{ cm_context }} SPINOZA_CM_CA="$PWD/.tmp/cm/tls.crt" \ go test "${args[@]}" ./{{ cm_dir }}/... test-cluster-mode-browser browser='chromium' mode='' name='': stub-assets cluster-mode-up cluster-mode-image cluster-mode-cover-prepare #!/usr/bin/env bash set -euo pipefail browser={{ quote(browser) }} case "$browser" in chromium|firefox|webkit) ;; *) echo "test-cluster-mode-browser: unsupported browser $browser" exit 1 ;; esac export SPINOZA_CM_COVERAGE=1 finish() { status=$? trap - EXIT if ! just cluster-mode-cover "cluster-mode-$browser"; then exit 1 fi exit "$status" } trap finish EXIT requested={{ quote(mode) }} modes=(oidc proxy) if [ -n "$requested" ]; then case "$requested" in oidc|proxy) modes=("$requested") ;; *) echo "test-cluster-mode-browser: unsupported mode $requested" exit 1 ;; esac fi if [ ! -d e2e/node_modules ] || [ e2e/package-lock.json -nt e2e/node_modules ]; then npm --prefix e2e ci fi install_deps=no if [ -n "${CI:-}" ]; then if command -v sudo > /dev/null 2>&1; then if sudo -n true > /dev/null 2>&1; then install_deps=yes fi fi fi if [ "$install_deps" = yes ]; then npx --prefix e2e playwright install --with-deps "$browser" else npx --prefix e2e playwright install "$browser" fi executable="$(node -e "console.log(require('./e2e/node_modules/playwright').${browser}.executablePath())")" if [ ! -x "$executable" ]; then npx --prefix e2e playwright install --force "$browser" fi export SPINOZA_CM_CONTEXT={{ cm_context }} export SPINOZA_CM_CA="$PWD/.tmp/cm/tls.crt" export SPINOZA_CM_BROWSER="$browser" name={{ quote(name) }} for current_mode in "${modes[@]}"; do SPINOZA_CM_BROWSER_FIXTURE="$current_mode" \ go test -tags clustermode -count=1 -timeout 20m -v \ -run '^TestTheBrowserFixtureIsReady$' ./{{ cm_dir }}/... run=(test --config=e2e/cluster-mode.config.ts) if [ -n "$name" ]; then run+=(--grep "$name") fi SPINOZA_CM_AUTH_MODE="$current_mode" npx --prefix e2e playwright "${run[@]}" done test-cluster-mode-release previous current: cluster-mode-up #!/usr/bin/env bash set -euo pipefail previous={{ quote(previous) }} current={{ quote(current) }} if [ -z "$previous" ] || [ -z "$current" ]; then echo "test-cluster-mode-release: previous and current versions are required" exit 1 fi SPINOZA_CM_CONTEXT={{ cm_context }} \ SPINOZA_CM_CA="$PWD/.tmp/cm/tls.crt" \ SPINOZA_CM_CHART=oci://ghcr.io/sophotechlabs/charts/spinoza \ SPINOZA_CM_USE_CHART_IMAGE=1 \ SPINOZA_CM_PREVIOUS_VERSION="$previous" \ SPINOZA_CM_CURRENT_VERSION="$current" \ go test -tags clustermode -count=1 -timeout 60m -v \ -run '^TestOnePublishedReleaseCanBeUpgradedAndRolledBack$' ./{{ cm_dir }}/... e2e-diagnostics: #!/usr/bin/env bash set -u output="$PWD/e2e/.tmp/diagnostics" mkdir -p "$output" if ! kind get clusters | grep -qx {{ test_cluster }}; then echo "no kind cluster {{ test_cluster }}" > "$output/cluster.txt" exit 0 fi kubectl --context {{ test_context }} get nodes -o wide > "$output/cluster.txt" 2>&1 kubectl --context {{ test_context }} get pods -A -o wide > "$output/pods.txt" 2>&1 kubectl --context {{ test_context }} get events -A --sort-by=.lastTimestamp > "$output/events.txt" 2>&1 kubectl --context {{ test_context }} -n argocd describe deployments,pods > "$output/argocd.txt" 2>&1 kubectl --context {{ test_context }} -n flux-system describe deployments,pods > "$output/flux.txt" 2>&1 helm --kube-context {{ test_context }} list -A > "$output/helm-list.txt" 2>&1 for node in $(kind get nodes --name {{ test_cluster }}); do docker exec "$node" crictl images > "$output/images-$node.txt" 2>&1 done cluster-mode-diagnostics: #!/usr/bin/env bash set -u output="$PWD/.tmp/cm/diagnostics" mkdir -p "$output" kubectl --context {{ cm_context }} get nodes,namespaces -o wide > "$output/cluster.txt" 2>&1 kubectl --context {{ cm_context }} get all,ingress,pvc -A -o wide > "$output/resources.txt" 2>&1 kubectl --context {{ cm_context }} get events -A --sort-by=.lastTimestamp > "$output/events.txt" 2>&1 kubectl --context {{ cm_context }} -n spinoza logs deployment/spinoza --all-containers --tail=-1 > "$output/spinoza.log" 2>&1 kubectl --context {{ cm_context }} -n spinoza logs deployment/oauth2-proxy --all-containers --tail=-1 > "$output/oauth2-proxy.log" 2>&1 kubectl --context {{ cm_context }} -n keycloak logs deployment/keycloak --all-containers --tail=-1 > "$output/keycloak.log" 2>&1 helm --kube-context {{ cm_context }} list -A > "$output/helm-list.txt" 2>&1 helm --kube-context {{ cm_context }} history spinoza -n spinoza > "$output/helm-history.txt" 2>&1 lint-chart: helm lint deploy/helm/spinoza --set publicURL=https://spinoza.example.com --set auth.mode=none --set auth.allowAnonymous=true helm template spinoza deploy/helm/spinoza --namespace spinoza \ --set publicURL=https://spinoza.example.com \ --set auth.mode=none --set auth.allowAnonymous=true \ --show-only templates/rbac.yaml | yq -e 'select(.kind == "ClusterRole") | ([.rules[] | select((.resources | contains(["secrets"])) or ((.verbs | contains(["impersonate"])) and ((.resources | contains(["users"])) or (.resources | contains(["groups"])) or (.resources | contains(["serviceaccounts"])))))] | length == 0) | select(. == true)' > /dev/null ! helm template spinoza deploy/helm/spinoza --namespace spinoza \ --set publicURL=https://spinoza.example.com > /dev/null 2>&1 ! helm template spinoza deploy/helm/spinoza --namespace spinoza \ --set publicURL=https://spinoza.example.com \ --set auth.mode=none > /dev/null 2>&1 ! helm template spinoza deploy/helm/spinoza --namespace spinoza \ --set publicURL=http://spinoza.example.com \ --set auth.mode=none --set auth.allowAnonymous=true > /dev/null 2>&1 helm template spinoza deploy/helm/spinoza --namespace spinoza \ --set publicURL=http://spinoza.example.com \ --set unsafeAllowHTTP=true \ --set auth.mode=none --set auth.allowAnonymous=true > /dev/null helm template spinoza deploy/helm/spinoza --namespace spinoza \ --set publicURL=https://spinoza.example.com \ --set auth.mode=oidc \ --set auth.oidc.issuerURL=https://keycloak.example.com/realms/main \ --set auth.oidc.clientID=spinoza > /dev/null ! helm template spinoza deploy/helm/spinoza --namespace spinoza \ --set publicURL=https://spinoza.example.com \ --set auth.mode=oidc \ --set auth.oidc.issuerURL=https://keycloak.example.com/realms/main \ --set auth.oidc.clientID=spinoza \ --set impersonate=true > /dev/null 2>&1 ! helm template spinoza deploy/helm/spinoza --namespace spinoza \ --set publicURL=https://spinoza.example.com \ --set auth.mode=oidc \ --set auth.oidc.issuerURL=http://keycloak.sso.svc/realms/main \ --set auth.oidc.clientID=spinoza > /dev/null 2>&1 helm template spinoza deploy/helm/spinoza --namespace spinoza \ --set publicURL=https://spinoza.example.com \ --set auth.mode=oidc \ --set auth.oidc.issuerURL=http://keycloak.sso.svc/realms/main \ --set auth.oidc.clientID=spinoza \ --set auth.oidc.unsafeAllowHTTP=true > /dev/null helm template spinoza deploy/helm/spinoza --namespace spinoza \ --set publicURL=https://spinoza.example.com \ --set auth.mode=oidc \ --set auth.oidc.issuerURL=https://keycloak.example.com/realms/main \ --set auth.oidc.clientID=spinoza \ --set impersonate=true \ --set 'rbac.impersonation.users[0]=alice@example.com' \ --set 'rbac.impersonation.groups[0]=platform' \ --show-only templates/rbac.yaml | yq -e 'select(.kind == "ClusterRole") | (([.rules[] | select(.resources | contains(["users"])) | .resourceNames[]] | .[0] == "alice@example.com") and ([.rules[] | select(.resources | contains(["groups"])) | .resourceNames[]] | .[0] == "platform") and ([.rules[] | select((.resources | contains(["serviceaccounts"])) and (.verbs | contains(["impersonate"])))] | length == 0)) | select(. == true)' > /dev/null ! helm template spinoza deploy/helm/spinoza --namespace spinoza \ --set publicURL=https://spinoza.example.com \ --set replicaCount=2 > /dev/null 2>&1 helm template spinoza deploy/helm/spinoza --namespace spinoza \ --set publicURL=https://spinoza.example.com \ --set auth.mode=proxy \ --set auth.proxy.sharedSecret=a-proxy-authentication-secret-that-is-long-enough \ --set rbac.read=workloads \ --set persistence.enabled=true \ --set ingress.enabled=true \ --set ingress.hosts[0].host=spinoza.example.com \ --set ingress.hosts[0].paths[0].path=/ > /dev/null test-release-publication: go test ./test/release test-docker-context: bash test/docker-context.sh image tag='spinoza:dev': docker build --build-arg SPINOZA_VERSION="$(just app-version)" -t {{ tag }} . lint: lint-be lint-fe lint-e2e lint-chart fmt-check: {{ golangci }} fmt --diff mod-check: go mod tidy -diff go mod verify dead: #!/usr/bin/env bash set -euo pipefail out=$(deadcode -test {{ go_pkgs }}) if [ -n "$out" ]; then echo "$out" exit 1 fi cross: #!/usr/bin/env bash set -euo pipefail for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64; do echo "cross: $target" GOOS="${target%/*}" GOARCH="${target#*/}" CGO_ENABLED=0 go build -o /dev/null . done for target in windows/amd64 windows/arm64; do echo "cross: $target desktop" GOOS="${target%/*}" GOARCH="${target#*/}" CGO_ENABLED=0 go build -tags desktop -o /dev/null . done audit-be: govulncheck {{ go_pkgs }} audit-fe: cd frontend && npm run knip cd frontend && npm run knip:production cd frontend && npm run depcheck cd frontend && npm run madge cd frontend && npm run typecov audit: audit-be audit-fe secrets: gitleaks dir . --no-banner --redact gitleaks git . --no-banner --redact # the run token cookie is served over loopback http only, so Secure would keep the # desktop webview from ever sending it back; see internal/server/guard.go sast_excluded := 'go.lang.security.audit.net.cookie-missing-secure.cookie-missing-secure' sast: semgrep scan --config .semgrep --config p/golang --config p/typescript --config p/react --exclude-rule {{ sast_excluded }} --error --quiet # e2e/fixtures holds workloads written to be insecure so the checks have # something to find; scanning them fails the build on findings we put there. vulns: vulnerability-exceptions #!/usr/bin/env bash set -euo pipefail report=$(mktemp) trap 'rm -f "$report"' EXIT if ! trivy fs --exit-code 1 --scanners secret,misconfig \ --format json --output "$report" \ --skip-dirs e2e/fixtures --skip-dirs test/clustermode --skip-dirs .tmp \ --skip-files test/integration/metrics-server.yaml \ --helm-set publicURL=https://spinoza.example.com \ --helm-set auth.mode=oidc \ --helm-set auth.oidc.issuerURL=https://idp.example.com/realms/spinoza \ --helm-set auth.oidc.clientID=spinoza \ --helm-set auth.sessionSecret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx .; then trivy convert --scanners secret,misconfig "$report" exit 1 fi bash test/trivy-helm-coverage.sh "$report" osv-scanner scan source --recursive --no-call-analysis=go . vulnerability-exceptions: stub-assets #!/usr/bin/env bash set -euo pipefail for tags in '' desktop desktop,production desktop,production,webkit2_41 integration clustermode; do args=() if [ -n "$tags" ]; then args=(-tags "$tags") fi deps=$(go list -deps "${args[@]}" ./...) if grep -q '^golang.org/x/crypto/openpgp\($\|/\)' <<< "$deps"; then echo "vulnerability-exceptions: the $tags build imports deprecated openpgp code" >&2 exit 1 fi if grep -q '^github.com/labstack/echo/v4\($\|/\)' <<< "$deps"; then echo "vulnerability-exceptions: the $tags build links echo, which only the wails dev server may use" >&2 exit 1 fi done echo "vulnerability-exceptions: no build imports deprecated openpgp code or links echo" workflows: scoped-tools workflow-triggers ci-tiers yamllint .forgejo .github actionlint -config-file .forgejo/actionlint.yaml .forgejo/workflows/*.yaml actionlint .github/workflows/*.yaml zizmor --no-online-audits --config .forgejo/zizmor.yml .forgejo/workflows/*.yaml zizmor --no-online-audits .github/workflows/*.yaml test/release-commit.sh test/release-pending.sh scoped-tools: #!/usr/bin/env bash set -euo pipefail unscoped=$(yq -r '.jobs[].steps[] | select(.uses // "" | test("jdx/mise-action")) | .with.install_args // "UNSCOPED"' \ .github/workflows/*.yaml .forgejo/workflows/*.yaml 2>/dev/null | grep -c UNSCOPED || true) if [ "$unscoped" != "0" ]; then echo "scoped-tools: $unscoped mise-action steps install every tool in mise.toml." echo "Name the tools the job runs with install_args, so an outage in a tool it" echo "never uses cannot fail it." exit 1 fi workflow-triggers: #!/usr/bin/env bash set -euo pipefail release_files=( .release-please-manifest.json CHANGELOG.md deploy/helm/spinoza/Chart.yaml wails.json ) mapfile -t validation < <(jq -r '.workflows | to_entries[] | select(.value.tier == "commit") | .key' .github/ci-tiers.json) for name in "${validation[@]}"; do workflow=".github/workflows/$name" ignored=$(yq -r '.on.pull_request.paths-ignore[]' "$workflow") for path in "${release_files[@]}"; do if ! grep -Fxq "$path" <<< "$ignored"; then echo "$workflow reruns for release-only change $path" >&2 exit 1 fi done done if [ "$(yq -r '.on | (has("pull_request") and has("workflow_call") and (has("pull_request_target") | not))' .github/workflows/e2e.yaml)" != "true" ]; then echo ".github/workflows/e2e.yaml does not support pull requests and trusted reuse" >&2 exit 1 fi ci-tiers: #!/usr/bin/env bash set -euo pipefail work=$(mktemp -d) trap 'rm -rf "$work"' EXIT for workflow in .github/workflows/*.yaml; do yq -o=json "$workflow" > "$work/$(basename "$workflow" .yaml).json" done node scripts/ci-tiers.mjs "$work" nightly-report dir sha url trigger coverage='': #!/usr/bin/env bash set -euo pipefail args=(--jobs {{ quote(dir) }}/jobs.json --out docs/ci/nightly --status {{ quote(dir) }}/status.json) args+=(--sha {{ quote(sha) }} --run-url {{ quote(url) }} --trigger {{ quote(trigger) }}) if [ -d {{ quote(dir) }}/reports ]; then args+=(--reports {{ quote(dir) }}/reports) fi if [ -d {{ quote(dir) }}/mutation ]; then args+=(--mutation {{ quote(dir) }}/mutation) fi if [ -d {{ quote(dir) }}/system-coverage ]; then args+=(--system-coverage {{ quote(dir) }}/system-coverage) fi coverage={{ quote(coverage) }} if [ -n "$coverage" ]; then args+=(--coverage "$coverage") fi node scripts/nightly-report.mjs "${args[@]}" hygiene: typos just editorconfig scripts/check-kubectl-skew.sh . shellcheck install.sh scripts/cluster-mode-coverage.sh scripts/check-kubectl-skew.sh scripts/check-mutation-report.sh scripts/check-mutation-total.sh scripts/create-kind-cluster.sh \ scripts/nightly-credential-helper.sh scripts/nightly-due.sh scripts/nightly-publish.sh \ scripts/release-commit.sh scripts/release-pending.sh \ test/release-commit.sh test/release-pending.sh test/install/container.sh \ test/install/uninstall.sh test/install/editorconfig-name.sh test/trivy-helm-coverage.sh \ test/docker-context.sh packaging/render.sh just --unstable --fmt --check editorconfig: #!/usr/bin/env bash set -euo pipefail for name in ec editorconfig-checker; do if command -v "$name" > /dev/null 2>&1; then exec "$name" fi done echo "no editorconfig checker on PATH; looked for ec and editorconfig-checker" >&2 exit 1 links: lychee --config lychee.toml . sbom: vulnerability-exceptions #!/usr/bin/env bash set -euo pipefail mkdir -p dist syft scan dir:. --source-name spinoza --exclude './frontend/node_modules/**' --output cyclonedx-json=dist/sbom.cdx.json grype sbom:dist/sbom.cdx.json --fail-on medium commits: #!/usr/bin/env bash set -euo pipefail from=$(node -p "try { const e = require(process.env.GITHUB_EVENT_PATH); (e.pull_request ? e.pull_request.base.sha : e.before) || '' } catch (e) { '' }") if [ -z "$from" ] || ! git cat-file -e "$from^{commit}" 2>/dev/null; then from=HEAD~1 fi npx --yes --package @commitlint/cli@21.2.2 --package @commitlint/config-conventional@21.2.2 commitlint --from "$from" --to HEAD fmt: {{ golangci }} fmt cd frontend && npm run format handoff-gate: stub-assets #!/usr/bin/env bash set -euo pipefail changed=$({ git diff --name-only HEAD; git ls-files --others --exclude-standard; } | sort -u) if [ -z "$changed" ]; then echo "handoff-gate: nothing changed against HEAD" fi if printf '%s\n' "$changed" | grep -qE '(\.go|^go\.mod|^go\.sum)$'; then just ci-go-lint go build ./... packages=$(printf '%s\n' "$changed" | sed -n 's|/[^/]*\.go$||p; s|^[^/]*\.go$|.|p' | sort -u) existing=() for package in $packages; do case "$package" in test/integration*) go vet -tags integration "./$package" continue ;; test/clustermode*) go vet -tags clustermode "./$package" continue ;; esac if [ -d "$package" ]; then existing+=("./$package") fi done if [ "${#existing[@]}" -gt 0 ]; then go test "${existing[@]}" fi fi if printf '%s\n' "$changed" | grep -q '^frontend/'; then if [ ! -d frontend/node_modules ] || [ frontend/package-lock.json -nt frontend/node_modules ]; then npm --prefix frontend ci fi just lint-fe npm --prefix frontend test fi if printf '%s\n' "$changed" | grep -q '^e2e/'; then just lint-e2e just validate-e2e-suite just test-e2e-scripts fi git diff --check ci-go-build: stub-assets cross go build ./... go build -tags desktop ./... ci-go-test: cover-gate ci-go-lint: lint-be fmt-check mod-check ci-go-audit: stub-assets audit-be dead ci-fe-lint: deps lint-fe ci-fe-test: deps test-fe ci-fe-audit: deps audit-fe ci-fe-build: deps cd frontend && npm run build just bundle-budget bundle-budget: #!/usr/bin/env bash set -euo pipefail budget=9000000 actual=$(find web/dist/assets -type f -exec cat {} + | wc -c | tr -d ' ') echo "bundle: $actual bytes (budget $budget)" if [ "$actual" -gt "$budget" ]; then exit 1 fi release-notes: #!/usr/bin/env bash set -euo pipefail mkdir -p dist git-cliff --latest --strip all --output dist/RELEASE_NOTES.md release-dist: deps #!/usr/bin/env bash set -euo pipefail shopt -s nullglob version=$(just app-version) export SPINOZA_VERSION="$version" mkdir -p dist/release cd frontend && npm run build && cd .. tar_sorts=no if tar --version 2>/dev/null | grep -q GNU; then tar_sorts=yes fi for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64; do goos="${target%/*}" goarch="${target#*/}" out="dist/build/${goos}_${goarch}" mkdir -p "$out" binary="spinoza" if [ "$goos" = "windows" ]; then binary="spinoza.exe" fi GOOS="$goos" GOARCH="$goarch" CGO_ENABLED=0 go build -trimpath -ldflags "{{ ldflags }} -X {{ version_pkg }}=$version" -o "$out/$binary" . cp LICENSE "$out/LICENSE" if [ "$goos" = "windows" ]; then archive="$PWD/dist/release/spinoza_${version}_${goos}_${goarch}.zip" rm -f "$archive" (cd "$out" && zip -q -X "$archive" "$binary" LICENSE) continue fi archive="dist/release/spinoza_${version}_${goos}_${goarch}.tar.gz" if [ "$tar_sorts" = yes ]; then tar --sort=name --owner=0 --group=0 --numeric-owner --mtime=@0 -cf - -C "$out" "$binary" LICENSE | gzip -n > "$archive" else tar -cf - -C "$out" "$binary" LICENSE | gzip -n > "$archive" fi done if [ -n "${SPINOZA_SKIP_EXTRAS:-}" ]; then echo "release-dist: SPINOZA_SKIP_EXTRAS is set, so no windows desktop app, deb or rpm" else just package-desktop-windows amd64 just package-desktop-windows arm64 just package-linux amd64 just package-linux arm64 fi syft scan dir:dist/build --source-name spinoza --source-version "$version" --output "cyclonedx-json=dist/release/spinoza_${version}_sbom.cdx.json" cd dist/release && sha256sum -- *.tar.gz *.zip *.deb *.rpm > checksums.txt cd ../.. just verify-checksums dist/release just verify-archives dist/release package-linux arch='amd64': #!/usr/bin/env bash set -euo pipefail version=$(just app-version) numeric="${version#v}" numeric="${numeric%%-*}" if ! printf '%s' "$numeric" | grep -Eq '^[0-9]+(\.[0-9]+){2}$'; then echo "package-linux: $version does not carry a release number, so no deb or rpm was built" exit 0 fi binary="dist/build/linux_{{ arch }}/spinoza" if [ ! -f "$binary" ]; then echo "package-linux: $binary is missing, run just release-dist first" exit 1 fi export SPINOZA_ARCH='{{ arch }}' export SPINOZA_NUMERIC="$numeric" mkdir -p dist/release dist/nfpm cp "$binary" dist/nfpm/spinoza for packager in deb rpm; do nfpm package --config packaging/nfpm.yaml --packager "$packager" --target "dist/release/spinoza_${version}_linux_{{ arch }}.${packager}" done rm -rf dist/nfpm verify-archives dir: #!/usr/bin/env bash set -euo pipefail shopt -s nullglob dir='{{ dir }}' checked=0 for file in "$dir"/*_windows_*.zip; do want=spinoza.exe case "$file" in *_app.zip) want=Spinoza.exe ;; esac listing=$(unzip -l "$file") if ! printf '%s\n' "$listing" | grep -q " ${want}$"; then echo "verify-archives: $file carries no $want" exit 1 fi checked=$((checked + 1)) done for file in "$dir"/*_linux_*.tar.gz "$dir"/*_darwin_*.tar.gz; do want=spinoza case "$file" in *_app.tar.gz) want=Spinoza ;; esac listing=$(tar -tzf "$file") if ! printf '%s\n' "$listing" | grep -qx "$want"; then echo "verify-archives: $file carries no $want" exit 1 fi checked=$((checked + 1)) done if [ "$checked" -eq 0 ]; then echo "verify-archives: $dir holds no archives to check" exit 1 fi echo "verify-archives: all $checked archives carry the binary their installer looks for" test-verify-archives: #!/usr/bin/env bash set -euo pipefail out=$(mktemp -d) trap 'rm -rf "$out"' EXIT cli="$out/cli" app="$out/app" release="$out/release" mkdir -p "$cli" "$app" "$release" head -c 1048576 /dev/urandom > "$cli/spinoza" cp "$cli/spinoza" "$cli/spinoza.exe" cp "$cli/spinoza" "$app/Spinoza" cp "$cli/spinoza" "$app/Spinoza.exe" cp LICENSE "$cli/LICENSE" cp LICENSE "$app/LICENSE" tar -cf - -C "$cli" spinoza LICENSE | gzip -n > "$release/spinoza_v9.9.9_linux_amd64.tar.gz" tar -cf - -C "$cli" spinoza LICENSE | gzip -n > "$release/spinoza_v9.9.9_darwin_arm64.tar.gz" tar -cf - -C "$app" Spinoza LICENSE | gzip -n > "$release/spinoza_v9.9.9_linux_amd64_app.tar.gz" if command -v zip > /dev/null 2>&1; then (cd "$cli" && zip -q -X "$release/spinoza_v9.9.9_windows_amd64.zip" spinoza.exe LICENSE) (cd "$app" && zip -q -X "$release/spinoza_v9.9.9_windows_amd64_app.zip" Spinoza.exe LICENSE) else (cd "$cli" && python3 -m zipfile -c "$release/spinoza_v9.9.9_windows_amd64.zip" spinoza.exe LICENSE) (cd "$app" && python3 -m zipfile -c "$release/spinoza_v9.9.9_windows_amd64_app.zip" Spinoza.exe LICENSE) fi just verify-archives "$release" tar -cf - -C "$cli" LICENSE | gzip -n > "$release/spinoza_v9.9.9_linux_amd64.tar.gz" if just verify-archives "$release" > /dev/null 2>&1; then echo "test-verify-archives: a tarball that lost its binary still passed" exit 1 fi echo "test-verify-archives: the check reads a whole listing and still fails an archive that lost its binary" verify-checksums dir: #!/usr/bin/env bash set -euo pipefail dir='{{ dir }}' list="$dir/checksums.txt" hash_of() { if command -v sha256sum > /dev/null; then sha256sum "$1" | awk '{ print $1 }' return fi shasum -a 256 "$1" | awk '{ print $1 }' } if [ ! -f "$list" ]; then echo "verify-checksums: $list is missing" exit 1 fi if ! awk '$2 ~ "/" { bad = 1 } END { exit bad }' "$list"; then echo "verify-checksums: a name in checksums.txt carries a path, install.sh matches bare filenames" exit 1 fi checked=0 for file in "$dir"/*.tar.gz "$dir"/*.tgz "$dir"/*.zip "$dir"/*.deb "$dir"/*.rpm; do if [ ! -f "$file" ]; then continue fi name=$(basename "$file") expected=$(awk -v name="$name" '$2 == name { print $1 }' "$list") if [ -z "$expected" ]; then echo "verify-checksums: $name is not listed in checksums.txt" exit 1 fi actual=$(hash_of "$file") if [ "$expected" != "$actual" ]; then echo "verify-checksums: $name does not match the checksum listed for it" exit 1 fi checked=$((checked + 1)) done if [ "$checked" -eq 0 ]; then echo "verify-checksums: $dir holds no artifacts to check" exit 1 fi echo "verify-checksums: every artifact in $dir matches checksums.txt" test-verify-checksums: #!/usr/bin/env bash set -euo pipefail out=$(mktemp -d) trap 'rm -rf "$out"' EXIT artifact="$out/spinoza-9.9.9.tgz" printf 'chart' > "$artifact" if command -v sha256sum > /dev/null; then (cd "$out" && sha256sum "$(basename "$artifact")" > checksums.txt) else (cd "$out" && shasum -a 256 "$(basename "$artifact")" > checksums.txt) fi just verify-checksums "$out" printf 'changed' >> "$artifact" if just verify-checksums "$out" > /dev/null 2>&1; then echo "test-verify-checksums: a changed Helm chart still passed" exit 1 fi echo "test-verify-checksums: Helm charts are checked and checksum mismatches fail" wait-for-release: #!/usr/bin/env bash set -euo pipefail version="${SPINOZA_VERSION:-}" if [ -z "$version" ]; then exit 0 fi url="https://github.com/sophotechlabs/spinoza/releases/download/$version/checksums.txt" for _ in $(seq 1 60); do if curl -fsSL -o /dev/null "$url"; then echo "wait-for-release: $version is downloadable" exit 0 fi sleep 5 done echo "wait-for-release: $url did not become downloadable within five minutes" exit 1 test-install: #!/usr/bin/env bash set -euo pipefail image="${IMAGE:?IMAGE is required}" expect="${EXPECT:-install}" args=(--rm) args+=(-e "SETUP=${SETUP:-}") args+=(-e "SPINOZA_VERSION=${SPINOZA_VERSION:-}") args+=(-v "$PWD/install.sh:/install.sh:ro") args+=(-v "$PWD/test/install/container.sh:/container.sh:ro") args+=("$image" sh /container.sh) if [ "$expect" = install ]; then docker run "${args[@]}" echo "test-install: $image installed and ran spinoza" exit 0 fi set +e output=$(docker run "${args[@]}" 2>&1) code=$? set -e if [ "$code" -eq 0 ]; then echo "test-install: $image installed spinoza, but this image has no downloader and should have refused" exit 1 fi if ! printf '%s' "$output" | grep -q 'neither curl nor wget is on PATH'; then echo "test-install: $image failed for a reason the test did not expect" printf '%s\n' "$output" exit 1 fi echo "test-install: $image refused to install without curl or wget" test-uninstall: sh test/install/uninstall.sh install.sh sh test/install/editorconfig-name.sh test-install-host: #!/usr/bin/env bash set -euo pipefail sh install.sh export PATH="$HOME/.local/bin:$PATH" spinoza --version if [ "$(uname -s)" != Darwin ]; then exit 0 fi app="" for candidate in /Applications "$HOME/Applications"; do if [ -d "$candidate/Spinoza.app" ]; then app="$candidate/Spinoza.app" break fi done if [ -z "$app" ]; then echo "test-install-host: the desktop app was not installed" exit 1 fi if ! lipo -archs "$app/Contents/MacOS/Spinoza" | grep -q x86_64; then echo "test-install-host: $app is not universal, intel macs cannot run it" exit 1 fi echo "test-install-host: installed $app as a universal binary" SPINOZA_UNINSTALL=1 sh install.sh if [ -e "$HOME/.local/bin/spinoza" ]; then echo "test-install-host: the binary is still there after the uninstall" exit 1 fi if [ -d "$app" ]; then echo "test-install-host: $app is still there after the uninstall" exit 1 fi echo "test-install-host: uninstalled the binary and the app" smoke: #!/usr/bin/env bash set -euo pipefail binary="dist/build/$(go env GOOS)_$(go env GOARCH)/spinoza" if [ ! -x "$binary" ]; then echo "smoke: $binary is missing, run just release-dist first" exit 1 fi "$binary" --version work=$(mktemp -d) printf 'apiVersion: v1\nkind: Config\n' > "$work/kubeconfig" KUBECONFIG="$work/kubeconfig" "$binary" --addr 127.0.0.1:34988 --token-file "$work/token" & pid=$! trap 'kill "$pid" 2>/dev/null || true; rm -rf "$work"' EXIT for _ in $(seq 1 50); do if [ -s "$work/token" ]; then break fi sleep 0.2 done if [ ! -s "$work/token" ]; then echo "smoke: the token file was never written" exit 1 fi token=$(cat "$work/token") for _ in $(seq 1 50); do code=$(curl -s -o /dev/null -w '%{http_code}' -H "X-Spinoza-Token: $token" http://127.0.0.1:34988/healthz || true) if [ "$code" = 200 ]; then break fi sleep 0.2 done if [ "$code" != 200 ]; then echo "smoke: healthz answered $code" exit 1 fi page=$(curl -s -H "X-Spinoza-Token: $token" http://127.0.0.1:34988/ || true) if printf '%s' "$page" | grep -q 'built without its frontend'; then echo "smoke: the binary embeds the placeholder index.html" exit 1 fi echo "smoke: $("$binary" --version) answered healthz and served the frontend" smoke-windows binary='spinoza.exe': pwsh -NoProfile -ExecutionPolicy Bypass -File test/smoke.ps1 -Binary "{{ binary }}" smoke-desktop-windows: #!/usr/bin/env bash set -euo pipefail version=$(just app-version) go build -trimpath -tags desktop -ldflags "{{ ldflags }} -X {{ version_pkg }}=$version" -o Spinoza.exe . reported=$(./Spinoza.exe --version) rm -f Spinoza.exe if [ "$reported" != "$version" ]; then echo "smoke-desktop-windows: the desktop build reports $reported, not $version" exit 1 fi echo "smoke-desktop-windows: the desktop build starts and reports $reported" test-install-windows shell='pwsh': {{ shell }} -NoProfile -ExecutionPolicy Bypass -File test/install/windows.ps1 test-ps shell='pwsh': {{ shell }} -NoProfile -ExecutionPolicy Bypass -File test/pester.ps1 lint-ps: pwsh -NoProfile -ExecutionPolicy Bypass -File test/lint-powershell.ps1 ci-windows-smoke: build just smoke-windows package-desktop: build-desktop #!/usr/bin/env bash set -euo pipefail version=$(just app-version) bundle=build/bin/spinoza.app if [ ! -d "$bundle" ]; then echo "package-desktop: $bundle was not built" exit 1 fi staged=dist/app rm -rf "$staged" mkdir -p "$staged" dist/release cp -R "$bundle" "$staged/Spinoza.app" if ! lipo -archs "$staged/Spinoza.app/Contents/MacOS/Spinoza" | grep -q x86_64; then echo "package-desktop: the bundle is not universal, intel macs cannot run it" exit 1 fi codesign --force --deep --sign - "$staged/Spinoza.app" codesign --verify --deep "$staged/Spinoza.app" ditto -c -k --keepParent "$staged/Spinoza.app" "dist/release/spinoza_${version}_darwin_app.zip" packaging: SPINOZA_VERSION="$(just app-version)" sh packaging/render.sh package-manifests: #!/usr/bin/env bash set -euo pipefail version=$(just app-version) just packaging mkdir -p dist/release archive="$PWD/dist/release/spinoza_${version}_packaging.tar.gz" rm -f "$archive" tar -cf - -C dist/packaging . | gzip -n > "$archive" echo "package-manifests: wrote $archive" test-packaging: #!/usr/bin/env bash set -euo pipefail out=$(mktemp -d) trap 'rm -rf "$out"' EXIT SPINOZA_VERSION=v9.9.9 CHECKSUMS=test/packaging/checksums.txt OUT_DIR="$out" sh packaging/render.sh yq -e '.version == "9.9.9"' "$out/scoop/spinoza.json" > /dev/null yq -e '.architecture["64bit"].hash == "0000000000000000000000000000000000000000000000000000000000000001"' "$out/scoop/spinoza.json" > /dev/null yq -e '.spec.version == "v9.9.9"' "$out/krew/spinoza.yaml" > /dev/null yq -e '.spec.platforms | length == 6' "$out/krew/spinoza.yaml" > /dev/null for file in "$out"/winget/*.yaml; do yq -e '.PackageVersion == "9.9.9"' "$file" > /dev/null done yq -p xml -o yaml -e '.package.metadata.version == "9.9.9"' "$out/chocolatey/spinoza.nuspec" > /dev/null grep -q 'version "9.9.9"' "$out/homebrew/spinoza.rb" grep -q '0000000000000000000000000000000000000000000000000000000000000004' "$out/homebrew/spinoza.rb" yq -e '.bin == "spinoza.exe"' "$out/scoop/spinoza.json" > /dev/null yq -e '.autoupdate.hash.url | test("checksums.txt$")' "$out/scoop/spinoza.json" > /dev/null yq -e '.apiVersion == "krew.googlecontainertools.github.com/v1alpha2"' "$out/krew/spinoza.yaml" > /dev/null yq -e '[.spec.platforms[] | select(.uri and .sha256 and .bin and .selector.matchLabels.os)] | length == 6' "$out/krew/spinoza.yaml" > /dev/null yq -e '.PackageIdentifier == "Sophotech.Spinoza"' "$out/winget/Sophotech.Spinoza.installer.yaml" > /dev/null yq -e '.NestedInstallerFiles[0].RelativeFilePath == "spinoza.exe"' "$out/winget/Sophotech.Spinoza.installer.yaml" > /dev/null yq -e '[.Installers[] | select(.InstallerUrl and .InstallerSha256)] | length == 2' "$out/winget/Sophotech.Spinoza.installer.yaml" > /dev/null for file in "$out"/winget/*.yaml; do yq -e '.ManifestType and .ManifestVersion' "$file" > /dev/null done yq -p xml -o yaml -e '.package.metadata.id == "spinoza"' "$out/chocolatey/spinoza.nuspec" > /dev/null grep -q 'checksumType64 *= *.sha256.' "$out/chocolatey/tools/chocolateyinstall.ps1" echo "test-packaging: every manifest rendered with the fields and hashes its ecosystem needs" publish-asset pattern: #!/usr/bin/env bash set -euo pipefail tag="${TAG:?TAG is required}" asset=$(ls dist/release/{{ pattern }}) name=$(basename "$asset") gh release upload "$tag" "$asset" --clobber gh release download "$tag" --pattern checksums.txt --output dist/release/checksums.remote.txt --clobber grep -v " ${name}$" dist/release/checksums.remote.txt > dist/release/checksums.merged.txt || true if command -v sha256sum > /dev/null; then (cd dist/release && sha256sum "$name" >> checksums.merged.txt) else (cd dist/release && shasum -a 256 "$name" >> checksums.merged.txt) fi sort -k2 dist/release/checksums.merged.txt > dist/release/checksums.txt just verify-checksums dist/release gh release upload "$tag" dist/release/checksums.txt --clobber publish-desktop: (publish-asset '*_darwin_app.zip') publish-desktop-linux: (publish-asset '*_linux_amd64_app.tar.gz') check: lint test ci: ci-go-build ci-go-test ci-go-lint ci-go-audit ci-fe-lint ci-fe-test ci-fe-audit ci-fe-build secrets sast vulns workflows hygiene lint-chart links sbom rescan: stub-assets audit-be vulns sbom links clean: rm -f spinoza spinoza.exe coverage.out rm -rf dist frontend/dist frontend/coverage web/dist/assets web/dist/index.html