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
15 changes: 4 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,11 @@ jobs:

# The ${VAR:?} guards in compose.yaml exist so a missing secret fails
# loudly at deploy time. CI supplies throwaway values so validation never
# needs a decryption key.
# needs a decryption key. The list of variables lives in the script, which
# scripts/validate.sh also calls — inlining it here is what let CI and the
# local run drift apart. The .env written here stays gitignored.
- name: Seed a validation-only .env
run: |
cp "$STACK/.env.example" "$STACK/.env"
{
echo "GRAFANA_ADMIN_PASSWORD=validation-only"
# RENDER_UID/GID are written by render-config.sh from the deploying
# user, so they are host-specific and absent from .env.example. Any
# value satisfies the ${VAR:?} guards that compose.yaml sets on them.
echo "RENDER_UID=65534"
echo "RENDER_GID=65534"
} >> "$STACK/.env"
run: ./scripts/seed-validation-env.sh "$STACK/.env"

# Single source of truth: whatever compose.yaml pins is what gets tested.
- name: Resolve pinned images from compose.yaml
Expand Down
53 changes: 53 additions & 0 deletions scripts/seed-validation-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
#
# Write a throwaway .env that satisfies compose.yaml's ${VAR:?} guards.
#
# The guards exist so a missing secret fails loudly at deploy time rather than
# starting a stack with defaults. Validation still has to get past them, so both
# CI and scripts/validate.sh need an .env holding values that are never
# deployed. That list used to be written out twice — once in
# .github/workflows/ci.yml and once in scripts/validate.sh — and it drifted the
# first time it changed: the commit adding the RENDER_UID/RENDER_GID guards
# updated only validate.sh, so it passed locally and failed in CI.
#
# Nothing written here is a secret and nothing here is ever deployed. The values
# only need to exist.
#
# Usage: scripts/seed-validation-env.sh <output-path>
#
# The caller owns the output path and its cleanup: CI writes the gitignored
# stacks/observability/.env, validate.sh writes an mktemp file it removes on
# exit. Keeping lifetime out here is what lets one script serve both.

set -euo pipefail

OUT="${1:?usage: seed-validation-env.sh <output-path>}"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
STACK="${REPO_ROOT}/stacks/observability"

cat "${STACK}/.env.example" > "${OUT}"
{
echo "GRAFANA_ADMIN_PASSWORD=validation-only"
# RENDER_UID/GID are written to .env by render-config.sh from the deploying
# user, so they are host-specific and deliberately absent from .env.example.
echo "RENDER_UID=65534"
echo "RENDER_GID=65534"
} >> "${OUT}"

# Sharing this script stops the two callers drifting from each other. This check
# stops both of them drifting from compose.yaml: a newly added ${VAR:?} guard
# fails here, naming the variable, instead of surfacing later as an opaque
# compose interpolation error in whichever caller runs first.
missing=()
while read -r var; do
[[ -n "${var}" ]] || continue
grep -qE "^${var}=" "${OUT}" || missing+=("${var}")
done < <(grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*:\?' "${STACK}/compose.yaml" \
| sed 's/^\${//; s/:?$//' | sort -u)

if ((${#missing[@]})); then
printf 'compose.yaml guards %s, which the validation .env does not set.\n' \
"${missing[*]}" >&2
printf 'Add a throwaway value for it to %s\n' "${BASH_SOURCE[0]}" >&2
exit 1
fi
19 changes: 7 additions & 12 deletions scripts/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,15 @@ have_docker() { have docker && docker info >/dev/null 2>&1; }
head_ "Compose"
# ---------------------------------------------------------------------------
if have docker; then
# A .env is required for the ${VAR:?} guards; use the example values plus
# throwaway secrets so validation does not depend on a decryption key.
# A .env is required for the ${VAR:?} guards; seeded by the same script CI
# uses, so a variable added there cannot pass locally and fail in CI. Written
# to a temp file rather than ${STACK}/.env so a local run never leaves an .env
# sitting next to a real one.
TMP_ENV="$(mktemp)"
trap 'rm -f "${TMP_ENV}"' EXIT
cat "${STACK}/.env.example" > "${TMP_ENV}"
{
echo "GRAFANA_ADMIN_PASSWORD=validation-only"
# RENDER_UID/GID come from the deploying user via render-config.sh and are
# host-specific, so they are deliberately not in .env.example. Any value
# validates — this only needs to satisfy the ${VAR:?} guards.
echo "RENDER_UID=65534"
echo "RENDER_GID=65534"
} >> "${TMP_ENV}"
if docker compose --env-file "${TMP_ENV}" -f "${STACK}/compose.yaml" config -q 2>/dev/null; then
if ! ./scripts/seed-validation-env.sh "${TMP_ENV}"; then
fail "seed a validation-only .env"
elif docker compose --env-file "${TMP_ENV}" -f "${STACK}/compose.yaml" config -q 2>/dev/null; then
pass "docker compose config"
else
docker compose --env-file "${TMP_ENV}" -f "${STACK}/compose.yaml" config -q
Expand Down
Loading