feat(dev-env): backport matured dev-env implementation from /etc/nixos

Replace the stub dev-env with the real, working implementation that grew
in the reference machine config — de-identified for the public scaffold.

Nix layer:
- options.nix: full project schema (url/upstream/fork/category/
  worktreeRoot/worktrees{branch,path,remote}/isClone/deployFlakeInput),
  deploy.targets, github.forkUser, writeDirenvHints. Drops the
  forgejo-URL block + deploy-flake auto-derivation (incoherent in a
  scaffold that uses explicit per-project urls).
- lib.nix: mkProject + worktreePath/bareRepoPath/projectRemotes,
  generalized to the explicit-url model (origin falls back to upstream).
- config.nix: renders /etc/dev-env/{config.sh,projects.json,
  tmux-sessions.json}, installs helpers via writeShellScriptBin, loads
  shell functions into interactive shells, wires the git pre-commit hook.

Scripts (config-driven, read /etc/dev-env at runtime):
- bootstrap.sh, nav.sh, worktree.sh, pr-helpers.sh, rebase.sh,
  status.sh, deploy.sh, regtest.sh, tmux-launch.sh.
- Stripped aiolabs/forgejo/bitspire/lamassu/webapp hardcoding; the
  github-fork remote is renamed 'fork' to match git.remotes vocabulary.
- Removes the dev.sh stub (the matured impl uses discrete commands +
  shell functions, not a unified 'dev' CLI).

presets/example.nix: a worked, generic project list replacing the
identity-specific aiolabs preset. tests/smoke.nix + flake checks
exercise the schema; 'nix flake check' is green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-15 21:18:49 +02:00
commit e38d313db2
17 changed files with 2925 additions and 147 deletions

View file

@ -0,0 +1,217 @@
#!/usr/bin/env bash
# dev-deploy: thin wrapper around your unified deploy flake
#
# Usage:
# dev-deploy <host> switch on host (uses locked deploy flake input)
# dev-deploy <host> test test build (no switch)
# dev-deploy <host> build local build only
# dev-deploy --local <host> same as switch but with --override-input
# wired up from local worktrees so the deploy
# builds against your in-progress changes
# dev-deploy all [test|build] every host in parallel
# dev-deploy update nix flake update
#
# Deploy host → SSH target mapping comes from /etc/dev-env/config.sh
# (devEnv.deploy.targets). The flake working copy lives at
# ${DEV_ROOT}/deploy[/<flakeInput>].
set -euo pipefail
if [[ -r /etc/dev-env/config.sh ]]; then
# shellcheck disable=SC1091
source /etc/dev-env/config.sh
fi
DEPLOY_BASE="${DEPLOY_DIR:-$DEV_ROOT/deploy}"
if [[ -n "${DEVENV_DEPLOY_FLAKE_INPUT:-}" ]]; then
DEPLOY_DIR="$DEPLOY_BASE/$DEVENV_DEPLOY_FLAKE_INPUT"
else
DEPLOY_DIR="$DEPLOY_BASE"
fi
PROJECTS_JSON="${DEVENV_PROJECTS_JSON:-/etc/dev-env/projects.json}"
usage() {
cat <<EOF
dev-deploy — wraps nixos-rebuild against your unified deploy flake
USAGE:
dev-deploy <host> deploy (switch)
dev-deploy <host> test test build (no switch)
dev-deploy <host> build local build only (no target)
dev-deploy --local <host> deploy with --override-input from local worktrees
dev-deploy --local <host> test test with overrides
dev-deploy all [test|build] every host in parallel
dev-deploy update [args...] nix flake update
Configured targets:
EOF
env | grep '^DEPLOY_TARGET_' | sed 's/^DEPLOY_TARGET_/ /' | sed 's/=/ → /' || true
if [[ -r "$DEPLOY_DIR/flake.nix" ]]; then
echo ""
echo "Backing flake: $DEPLOY_DIR"
fi
exit 1
}
[[ $# -lt 1 ]] && usage
# Pull the host → target map. We accept either:
# 1. DEPLOY_TARGET_<HOST>=<ssh> (rendered by config.nix as env vars)
# 2. ${DEPLOY_DIR}/deploy.sh's TARGETS associative array (fallback)
target_for() {
local host="$1"
local var="DEPLOY_TARGET_${host//-/_}"
if [[ -n "${!var:-}" ]]; then
echo "${!var}"
return
fi
# Fallback: parse a deploy.sh TARGETS array (simple regex; brittle
# but enough as a fallback)
if [[ -r "$DEPLOY_DIR/deploy.sh" ]]; then
awk -v h="$host" '
/^[[:space:]]*\[/ {
if (match($0, /\[([^]]+)\]="([^"]*)"/, m)) {
if (m[1] == h) { print m[2]; exit }
}
}
' "$DEPLOY_DIR/deploy.sh"
fi
}
# --- subcommands ---
cmd_update() {
shift
cd "$DEPLOY_DIR"
nix flake update "$@"
}
# Build the --override-input flags for a host using projects.json's
# `deployFlakeInput` mapping. For each project that has deployFlakeInput
# set AND a worktree resolved to a real on-disk path, we emit
# --override-input <input> path:<worktree-path>
# Picks the first worktree by default; pass DEVENV_OVERRIDE_WORKTREE=<name>
# to choose a specific one.
build_overrides() {
local args=()
local prefer="${DEVENV_OVERRIDE_WORKTREE:-}"
[[ -r "$PROJECTS_JSON" ]] || { printf '%s\n' ""; return; }
while IFS=$'\t' read -r input wt_path; do
[[ -z "$input" || "$input" == "null" ]] && continue
[[ -d "$wt_path" ]] || continue
args+=(--override-input "$input" "path:$wt_path")
done < <(jq -r --arg prefer "$prefer" '
to_entries[]
| select(.value.deployFlakeInput != null)
| .value as $v
| (if ($prefer | length) > 0 and ($v.worktrees | has($prefer))
then $v.worktrees[$prefer].path
else
($v.worktrees | to_entries | first.value.path // null)
end) as $path
| "\($v.deployFlakeInput)\t\($path // "")"
' "$PROJECTS_JSON")
printf '%s\0' "${args[@]}"
}
run_host() {
local host="$1" action="$2" use_local="$3"
local target
target="$(target_for "$host")"
if [[ -z "$target" ]] && [[ "$action" != "build" ]]; then
echo "no SSH target for host '$host' (set DEPLOY_TARGET_${host//-/_})" >&2
return 1
fi
cd "$DEPLOY_DIR"
local overrides=()
if $use_local; then
# Read the null-delimited overrides into an array
while IFS= read -r -d '' arg; do
[[ -n "$arg" ]] && overrides+=("$arg")
done < <(build_overrides)
if (( ${#overrides[@]} > 0 )); then
echo "[$host] using ${#overrides[@]} local override(s):"
local i=0
while (( i < ${#overrides[@]} )); do
echo " ${overrides[$((i+1))]} -> ${overrides[$((i+2))]}"
i=$((i + 3))
done
fi
fi
case "$action" in
deploy)
echo "[$host] switch on $target..."
nixos-rebuild switch \
--flake ".#$host" \
--target-host "$target" \
"${overrides[@]}"
;;
test)
echo "[$host] test on $target..."
nixos-rebuild test \
--flake ".#$host" \
--target-host "$target" \
"${overrides[@]}"
;;
build)
echo "[$host] local build..."
nix build \
".#nixosConfigurations.$host.config.system.build.toplevel" \
"${overrides[@]}"
;;
*)
echo "unknown action: $action" >&2
return 1
;;
esac
}
USE_LOCAL=false
if [[ "$1" == "--local" ]]; then
USE_LOCAL=true
shift
fi
case "$1" in
update)
cmd_update "$@"
;;
all)
action="${2:-deploy}"
# iterate every DEPLOY_TARGET_* env var
pids=()
hosts=()
for var in $(env | grep -o '^DEPLOY_TARGET_[A-Za-z0-9_]*' || true); do
host="${var#DEPLOY_TARGET_}"
host="${host//_/-}"
run_host "$host" "$action" "$USE_LOCAL" &
pids+=($!)
hosts+=("$host")
done
failed=()
for i in "${!pids[@]}"; do
if ! wait "${pids[$i]}"; then
failed+=("${hosts[$i]}")
fi
done
if (( ${#failed[@]} > 0 )); then
echo "FAILED: ${failed[*]}"
exit 1
fi
echo "All hosts deployed successfully."
;;
-h|--help)
usage
;;
*)
host="$1"
action="${2:-deploy}"
run_host "$host" "$action" "$USE_LOCAL"
;;
esac