Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@ High-level layout; see the Child NAD Index below for domain-specific details.
config used by background tasks (e.g. `SMTP_*`) must be passed to
`celery-worker` in `compose.yml`, not only to `backend` — otherwise it fails
silently in prod while request-path features (like the SMTP test) still work.
- **`.env` and `.env.development` drift out of date** as `.env.example` evolves.
Run `./nukelabctl check-env` to detect missing or stale keys, and
`./nukelabctl sync-env` to non-destructively append missing keys from
`.env.example` while preserving local values and secrets.
- **Production deployments must be version-pinned**. When `APP_ENV=production`,
`nukelabctl` refuses to boot if the resolved version is `0.0.0-dev`. Pin a
release with a `VERSION` file, a git tag, or an explicit `NUKELAB_VERSION`
/ `NUKELAB_IMAGE_TAG` (use the latter when pulling pre-built GHCR images).

## Security & penetration testing

Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ release version.
`DB_SCHEMA_GUARD` setting (`auto` refuse in production/warn elsewhere,
`enforce` always refuse, `off` disabled). If the database is unreachable the
guard logs a warning and does not block startup.
- Environment-file drift detection and repair: `./nukelabctl check-env` compares
`.env` / `.env.development` against `.env.example` and reports missing or
stale keys (optionally value differences with `--changed`, all files with
`--all`). `./nukelabctl sync-env` non-destructively appends missing keys from
`.env.example` while preserving existing local values and secrets; stale keys
are reported but left for manual removal. Both commands are read-only by
default unless `sync-env` is invoked with `--yes` or confirmed interactively.
- Production version guard: when `APP_ENV=production`, `nukelabctl` refuses to
boot if the resolved version is `0.0.0-dev`. Production requires a real
release via `VERSION`, a git tag, or an explicit `NUKELAB_VERSION` /
`NUKELAB_IMAGE_TAG` (the latter is useful when pulling pre-built GHCR
images).

### Changed

Expand Down
11 changes: 11 additions & 0 deletions nukelabctl
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,10 @@ ${BOLD}Maintenance:${RESET}
${GREEN}rotate-user-auth-key${RESET} Rotate the active user-auth Ed25519 key
${GREEN}cleanup-user-auth-keys${RESET} Remove expired retired user-auth keys

${BOLD}Environment:${RESET}
${GREEN}check-env${RESET} [options] Compare env files to .env.example
${GREEN}sync-env${RESET} [file] [options] Merge missing keys from .env.example

${BOLD}Development Tools:${RESET}
${GREEN}shell${RESET} [service] Open shell in container
${GREEN}exec${RESET} [service] [command] Execute command in container
Expand Down Expand Up @@ -320,6 +324,9 @@ ${BOLD}Examples:${RESET}
./nukelabctl init-user-auth-keys # Generate initial user-auth keys (production setup)
./nukelabctl rotate-user-auth-key # Rotate the active user-auth key
./nukelabctl cleanup-user-auth-keys # Prune expired retired public keys
./nukelabctl check-env # Check active env file for drift
./nukelabctl check-env --all --changed # Check all env files including value drift
./nukelabctl sync-env .env.development --dry-run # Preview missing keys

EOF
}
Expand Down Expand Up @@ -465,6 +472,10 @@ main() {
lint)
_dispatch_command "$CMD"
;;
check-env | sync-env)
# These commands only parse env files; no container engine or env state needed.
_dispatch_command "$CMD"
;;
status | logs | shell | exec | db-migrate | db-shell | backup | restore | e2e | security | doctor | init-user-auth-keys | rotate-user-auth-key | cleanup-user-auth-keys | verify-hardening)
_bootstrap restore
_dispatch_command "$CMD"
Expand Down
7 changes: 7 additions & 0 deletions scripts/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ All files under `scripts/`, plus the top-level `nukelabctl` dispatcher.
- `update` has a `--build` escape hatch that forces a source rebuild even when `NUKELAB_PULL_DEPLOY=true`. In source-build mode `update` and `pull` pull base images for the pullable infra services only, via `_pullable_infra_services`.
- `_pullable_infra_services` returns the pullable (non-buildable) infra services (`traefik postgres redis` + enabled overlay services) as a word-split list, mirroring `_backend_services`. App services (`backend`, `celery-worker`, `celery-beat`, `frontend`) are excluded so unpinned ghcr.io images are never pulled without registry auth; pull mode pulls them explicitly.
- Each management command exposes `cmd_<name>`, `help_<name>`, and `parse_<name>_args` when it accepts flags.
- Environment-file helpers live in `scripts/lib.sh`:
- `load_env_file` exports active KEY=VALUE lines from a file.
- `_read_env_into_assoc` reads active KEY=VALUE lines into an associative array without exporting.
- `_assoc_has_key` tests associative-array key membership.
- `check-env` and `sync-env` compare local env files against `.env.example`.
`check-env` is read-only; `sync-env` appends missing keys without overwriting
existing values and never removes stale keys without operator consent.
- Security scanning helpers live in `scripts/security/`.

## Work Guidance
Expand Down
62 changes: 58 additions & 4 deletions scripts/lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,47 @@ load_env_file() {
done < "$env_file"
}

# Usage: _read_env_into_assoc <file> <assoc_array_name>
# Reads active KEY=VALUE lines from <file> into the named associative array.
# Comments, blank lines, and malformed keys are ignored. An optional leading
# `export ` prefix is tolerated, and trailing inline comments are stripped
# (only when # is preceded by whitespace, matching load_env_file).
# The array is reset before populating; values are NOT exported.
_read_env_into_assoc() {
local env_file="$1"
local -n _assoc="$2"
_assoc=()
while IFS= read -r line || [[ -n "$line" ]]; do
[[ "$line" =~ ^[[:space:]]*#.*$ ]] && continue
[[ -z "${line// /}" ]] && continue

local cleaned="${line#export }"

if [[ "$cleaned" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)[[:space:]]+#.*$ ]]; then
cleaned="${BASH_REMATCH[1]}=${BASH_REMATCH[2]}"
while [[ "$cleaned" == *[[:space:]] ]]; do
cleaned="${cleaned%[[:space:]]}"
done
fi

local key="${cleaned%%=*}"
if [[ "$cleaned" != *=* ]] || [[ ! "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
continue
fi

local value="${cleaned#*=}"
_assoc["$key"]="$value"
done < "$env_file"
}

# Usage: _assoc_has_key <assoc_array_name> <key>
# Returns 0 if the associative array contains the given key, 1 otherwise.
_assoc_has_key() {
local -n _assoc="$1"
local key="$2"
[[ -v "_assoc[$key]" ]]
}

# Usage: init_env [dev_mode]
# In dev mode (.env.development present) the dev file is loaded FIRST so its
# values win: load_env_file never overwrites an already-set variable, so .env
Expand Down Expand Up @@ -210,6 +251,18 @@ init_env() {
export NUKELAB_VERSION="${_nv#v}"
fi

# Production guard: refuse to operate with the unresolved development
# fallback version. A production stack must be pinned to a real release
# via VERSION, a git tag, or an explicit NUKELAB_VERSION / NUKELAB_IMAGE_TAG.
if [ "${APP_ENV:-}" = "production" ] && [ "$NUKELAB_VERSION" = "0.0.0-dev" ]; then
die "Production deployments require a release version, but the resolved version is 0.0.0-dev.\n\n\
Resolve this by one of:\n\
- Create a VERSION file: echo '2.0.0' > VERSION\n\
- Check out a git tag (vX.Y.Z)\n\
- Set NUKELAB_VERSION explicitly in the environment or env file\n\
- Set NUKELAB_IMAGE_TAG explicitly when pulling pre-built images"
fi

# Default the registry image tag consumed by compose.yml image:
# substitutions. When the operator explicitly pinned NUKELAB_VERSION, use
# it as the image tag unless NUKELAB_IMAGE_TAG was pinned separately.
Expand Down Expand Up @@ -1334,7 +1387,7 @@ _ensure_venv_tool() {
# Resolve the NukeLab version string. Preference order:
# 1. $DIR/VERSION file (publishable artifact)
# 2. git describe --tags (e.g. v2.0, v2.0-3-gabc123)
# 3. hardcoded default (kept as a last-resort fallback)
# 3. hardcoded development fallback (must never look like a release)
#
# Lives in lib.sh (not scripts/manage.d/version.sh) because print_help() in
# nukelabctl calls it before any command module has been sourced.
Expand All @@ -1350,15 +1403,16 @@ _nukelab_version() {
if command -v git > /dev/null 2>&1 && [ -d "$DIR/.git" ]; then
# --tags only succeeds when at least one tag exists; --always is
# intentionally omitted so a bare short-sha never masks the
# hardcoded fallback default. The trailing `|| true` plus the `if`
# development fallback. The trailing `|| true` plus the `if`
# guard both neutralize the ERR trap inherited via `set -E` so a
# tag-less repo falls through to the v2.0 default instead of aborting.
# tag-less repo falls through to the 0.0.0-dev fallback instead of
# aborting.
if version=$(cd "$DIR" && git describe --tags 2> /dev/null || true); then
if [ -n "$version" ]; then
echo "$version"
return
fi
fi
fi
echo "v2.0"
echo "0.0.0-dev"
}
127 changes: 127 additions & 0 deletions scripts/manage.d/check-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/bin/bash
# SPDX-FileCopyrightText: 2023-2026 NukeHub Developers
# SPDX-License-Identifier: BSD-2-Clause

# Default options for check-env.
CHECK_ENV_ALL=false
CHECK_ENV_CHANGED=false

cmd_check_env() {
local target_files=()

if $CHECK_ENV_ALL; then
[ -f .env ] && target_files+=(".env")
[ -f .env.development ] && target_files+=(".env.development")
else
if $USE_DEV_MODE && [ -f .env.development ]; then
target_files+=(".env.development")
elif [ -f .env ]; then
target_files+=(".env")
elif [ -f .env.development ]; then
target_files+=(".env.development")
else
die "No environment file found.\n\n cp .env.example .env.development"
fi
fi

[ -f .env.example ] || die ".env.example not found"

declare -A example_values
_read_env_into_assoc .env.example example_values

local exit_code=0
for target in "${target_files[@]}"; do
step "Checking ${target} against .env.example..."

declare -A target_values
_read_env_into_assoc "$target" target_values

local missing=() stale=() changed=()

for key in "${!example_values[@]}"; do
if ! _assoc_has_key target_values "$key"; then
missing+=("$key")
fi
done

for key in "${!target_values[@]}"; do
if ! _assoc_has_key example_values "$key"; then
stale+=("$key")
fi
done

if $CHECK_ENV_CHANGED; then
for key in "${!target_values[@]}"; do
if _assoc_has_key example_values "$key"; then
if [ "${target_values[$key]}" != "${example_values[$key]}" ]; then
changed+=("$key")
fi
fi
done
fi

if [ ${#missing[@]} -eq 0 ] && [ ${#stale[@]} -eq 0 ] && [ ${#changed[@]} -eq 0 ]; then
ok "${target} is in sync with .env.example"
else
exit_code=1
if [ ${#missing[@]} -gt 0 ]; then
echo "${RED}✗${RESET} Missing keys in ${target}:"
printf ' - %s\n' "${missing[@]}" | sort
fi
if [ ${#stale[@]} -gt 0 ]; then
echo "${YELLOW}⚠${RESET} Stale keys in ${target} (removed from .env.example):"
printf ' - %s\n' "${stale[@]}" | sort
fi
if [ ${#changed[@]} -gt 0 ]; then
echo "${BLUE}▶${RESET} Changed values in ${target}:"
printf ' - %s\n' "${changed[@]}" | sort
fi
fi
done

exit $exit_code
}

parse_check_env_args() {
while [[ ${#EXTRA_ARGS[@]} -gt 0 ]]; do
case "${EXTRA_ARGS[0]}" in
--all)
CHECK_ENV_ALL=true
EXTRA_ARGS=("${EXTRA_ARGS[@]:1}")
;;
--changed)
CHECK_ENV_CHANGED=true
EXTRA_ARGS=("${EXTRA_ARGS[@]:1}")
;;
--help | -h)
help_check_env
exit 0
;;
--*)
die "Unknown option for check-env: ${EXTRA_ARGS[0]}"
;;
*)
die "Unexpected argument for check-env: ${EXTRA_ARGS[0]}"
;;
esac
done
}

help_check_env() {
cat <<- EOF
${BOLD}Usage:${RESET} ./nukelabctl check-env [options]

Compare .env / .env.development against the canonical .env.example template.
Reports missing keys, stale keys, and (with --changed) keys whose values differ
from the example defaults.

${BOLD}Options:${RESET}
--all Check both .env and .env.development if they exist
--changed Also report keys whose local value differs from .env.example
--help, -h Show this help

${BOLD}Examples:${RESET}
./nukelabctl check-env
./nukelabctl check-env --all --changed
EOF
}
Loading
Loading