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'
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