Skip to content
quarry Artifact [5f42b703b2]
Login

Artifact [5f42b703b2]

Artifact 5f42b703b245fcbeba9df83cbaab91afb3aa591c7cf8688314e01a7b66ae40a9:


#!/bin/sh
# deploy-test.sh — run deploy.sh, quarry-alert.sh and quarry-deploy-watch.sh
# through their paths against temp directories and stubs: no root, no
# host, no network, nothing outside one mktemp directory. Runs on Linux
# and macOS (a perl stand-in for util-linux flock where there is none).
#
#   sh scripts/deploy-test.sh      prints ok/FAIL per case; exit 1 on any FAIL
# Stub bodies are single-quoted on purpose (they expand when the stub
# runs), and each case line is a deliberate "check && ok || bad".
# shellcheck disable=SC2016,SC2015,SC2030,SC2031
set -u
here=$(cd "$(dirname "$0")" && pwd)
T=$(mktemp -d "${TMPDIR:-/tmp}/deploy-test.XXXXXX")
pids=
cleanup() {
  for p in $pids; do kill "$p" 2>/dev/null; wait "$p" 2>/dev/null; done
  rm -rf "$T"
}
trap cleanup EXIT
fails=0
ok() { echo "ok:   $1"; }
bad() { echo "FAIL: $1"; fails=$((fails + 1)); }

H=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
OTHER=fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210

# ---- stubs --------------------------------------------------------------
S=$T/stub
mkdir -p "$S"
C=$T  # the current case's directory; stubs write into it
export C
stub() { printf '#!/bin/sh\n%s\n' "$2" >"$S/$1"; chmod +x "$S/$1"; }
if ! command -v flock >/dev/null 2>&1; then
  # util-linux `flock -n FD`, via flock(2) on the inherited descriptor.
  stub flock '[ "$1" = -n ] || exit 64
exec perl -MFcntl=:flock -e '"'"'open(my $fh, ">&=", $ARGV[0]) or die; flock($fh, LOCK_EX|LOCK_NB) or exit 1'"'"' "$2"'
fi
stub sudo '[ "$1" = -u ] && shift 2; exec "$@"'
stub chown ':'
stub sleep ':'
stub df 'echo "Filesystem 1K-blocks Used Available"; echo "/dev/x 1 1 999999999"'
stub logger '[ "$1" = -t ] && shift 2; echo "$*" >>"$C/journal"'
stub systemctl 'echo "$*" >>"$C/systemctl"; case "$1" in cat) exit 0 ;; esac'
stub systemd-run 'echo "$*" >>"$C/systemd-run"; exit "$(cat "$C/systemd-run.rc" 2>/dev/null || echo 0)"'
# curl: the forge's API answers from files in the case directory.
stub curl 'url=; for a; do case "$a" in http*) url=$a ;; esac; done
case "$url" in
  */api/v1/r/*/checkin/*) [ -f "$C/checkin.json" ] || exit 22; cat "$C/checkin.json" ;;
  */api/v1/version) echo "{\"ready\":true}" ;;
  http://127.0.0.1:8950/) echo "<p>quarry 0.1.0</p>" ;;
esac
exit 0'
# docker: "builds" into the mounted tree as $C/build says:
#   full | partial (no SHA256SUMS, one missing) | none (dies before bin/runner)
stub docker 'w=; prev=; for a; do [ "$prev" = -v ] && case "$a" in *:/src) w=${a%:/src} ;; esac; prev=$a; done
mkdir -p "$w/bin"
printf "#!/bin/sh\necho quarry\n" >"$w/bin/quarry"; printf "#!/bin/sh\n" >"$w/bin/zit"; chmod +x "$w/bin/quarry" "$w/bin/zit"
mode=$(cat "$C/build")
[ "$mode" = none ] && exit 0
mkdir -p "$w/bin/runner"
for f in linux-amd64 linux-arm64 darwin-arm64 darwin-amd64 windows-amd64.exe; do echo "$f" >"$w/bin/runner/quarry-runner-$f"; done
if [ "$mode" = partial ]; then rm "$w/bin/runner/quarry-runner-linux-arm64"; exit 0; fi
cd "$w/bin/runner" && sha256sum quarry-runner-* >SHA256SUMS'

# One case = a fresh directory: a BIN holding a stub zit that opens a
# fixture tree at hash $C/hash, and a published runner set to keep.
#
# The stub answers exactly what zit 0.3.35 prints (pasted from a real
# scratch store): `open` one line, `info current` the full hash FIRST,
# bare `info` the checkout summary with "repository:" first — so a
# script that reads the wrong one fails here too. Anything else it was
# not told about is an error, not a silent pass. The real-zit cases at
# the end hold these fixtures to the binary.
setup() {
  C=$T/case.$1
  mkdir -p "$C/bin/runner" "$C/tmp" "$C/tree/scripts"
  echo old >"$C/bin/runner/SHA256SUMS"
  printf '#!/bin/sh\necho old\n' >"$C/bin/quarry"; chmod +x "$C/bin/quarry"
  echo "$H" >"$C/hash"
  echo full >"$C/build"
  echo "go 1.25" >"$C/tree/go.mod"
  cp "$here/build-linux.sh" "$here/runner-check.sh" "$here/deploy.sh" "$C/tree/scripts/"
  cat >"$C/bin/zit" <<ZIT
#!/bin/sh
h=\$(cat "$C/hash")
case "\$*" in
  "open trunk --store="*)
    cp -R "$C/tree/." .
    echo "opened \$h on trunk" ;;
  "info current")
    echo "\$h"
    echo "branch:  trunk"
    echo "leaf:    open" ;;
  info)
    echo "repository:   \$PWD/.zit"
    echo "local-root:   \$PWD/"
    echo "checkout:     \$(echo "\$h" | cut -c1-12) Wed 2026-09-30 15:41:05 UTC"
    echo "tags:         trunk"
    echo "comment:      one (user: t)"
    echo "check-ins:    1" ;;
  *) echo "zit stub: no fixture for: \$*" >&2; exit 1 ;;
esac
ZIT
  chmod +x "$C/bin/zit"
}
deploy() {
  (
    export PATH="$S:$PATH" C QUARRY_DEPLOY_BIN="$C/bin" QUARRY_DEPLOY_STORE="$C/store" \
      QUARRY_DEPLOY_ZITREL="$C/zitrel" QUARRY_DEPLOY_TMP="$C/tmp" \
      QUARRY_DEPLOY_LOCK="$C/lock" QUARRY_DEPLOY_STATE="$C/state"
    sh "$here/deploy.sh" "$@" >"$C/out" 2>&1
  )
  rc=$?
}
journal() { grep -q "$1" "$C/journal" 2>/dev/null; }

# ---- --expect -----------------------------------------------------------
setup e12; deploy --dry-run --expect "$(echo "$H" | cut -c1-12)"
[ "$rc" = 0 ] && ok "a 12-character --expect matching trunk deploys" || bad "12-char expect match: rc=$rc $(tail -1 "$C/out")"
setup e64; deploy --dry-run --expect "$H"
[ "$rc" = 0 ] && ok "a full 64-character --expect matching trunk deploys" || bad "64-char expect match: rc=$rc $(tail -1 "$C/out")"
setup m12; deploy --dry-run --expect "$(echo "$OTHER" | cut -c1-12)"
[ "$rc" = 1 ] && journal 'FAILED stage=open .*trunk moved' && ok "a 12-character --expect of another check-in is refused" || bad "12-char moved: rc=$rc"
setup m64; deploy --dry-run --expect "$OTHER"
[ "$rc" = 1 ] && journal 'FAILED stage=open .*trunk moved' && ok "a 64-character --expect of another check-in is refused" || bad "64-char moved: rc=$rc"
setup short; deploy --dry-run --expect 0123456789a
[ "$rc" = 2 ] && ok "an --expect under 12 characters is a usage error" || bad "short expect: rc=$rc"
setup nothex; deploy --dry-run --expect 0123456789aZ
[ "$rc" = 2 ] && ok "a non-hex --expect is a usage error" || bad "non-hex expect: rc=$rc"

# ---- the lock -----------------------------------------------------------
setup lock
( exec 9>>"$C/lock"; PATH="$S:$PATH" flock -n 9 && echo "since then (pid 1, id 1-1)" >"$C/lock" && touch "$C/held" && /bin/sleep 3 ) &
while [ ! -f "$C/held" ]; do /bin/sleep 0.1; done
deploy --dry-run
wait
[ "$rc" = 1 ] && grep -q "another deploy is running — since then" "$C/out" && ok "a second deploy is refused, saying since when" || bad "lock: rc=$rc $(cat "$C/out")"

# ---- --require-green ----------------------------------------------------
setup red; echo '{"required":["quarry","race"],"unmet":["race"]}' >"$C/checkin.json"; deploy --dry-run --require-green
[ "$rc" = 1 ] && journal 'FAILED stage=green .*race' && ok "--require-green refuses a red required check" || bad "red: rc=$rc"
setup green; echo '{"required":["quarry","race"],"unmet":[]}' >"$C/checkin.json"; deploy --dry-run --require-green
[ "$rc" = 0 ] && ok "--require-green passes when every required check is green" || bad "green: rc=$rc $(tail -1 "$C/out")"
setup noreq; echo '{"required":[],"unmet":[]}' >"$C/checkin.json"; deploy --dry-run --require-green
[ "$rc" = 1 ] && journal 'FAILED stage=green .*requires no checks' && ok "--require-green refuses a trunk that requires nothing" || bad "noreq: rc=$rc"
setup oldforge; echo '{"checks":[]}' >"$C/checkin.json"; deploy --dry-run --require-green
[ "$rc" = 1 ] && journal 'FAILED stage=green' && ok "--require-green refuses a forge that does not say" || bad "oldforge: rc=$rc"

# ---- the build and the runner set ---------------------------------------
setup died; echo none >"$C/build"; deploy
[ "$rc" = 1 ] && journal 'FAILED stage=build .*died part-way' && grep -q "echo old" "$C/bin/quarry" \
  && ok "a build that dies before the runner downloads fails, installing nothing" || bad "died: rc=$rc $(tail -1 "$C/out")"
setup pre; echo none >"$C/build"; grep -v '^RUNNER_TARGETS=' "$here/build-linux.sh" >"$C/tree/scripts/build-linux.sh"; deploy
[ "$rc" = 0 ] && journal '^OK ' && [ "$(cat "$C/bin/runner/SHA256SUMS")" = old ] && ok "a tree that builds no runner downloads deploys and keeps the published set" || bad "pre: rc=$rc $(tail -1 "$C/out")"
setup partial; echo partial >"$C/build"; deploy
[ "$rc" = 1 ] && journal 'FAILED stage=runner ' && [ "$(cat "$C/bin/runner/SHA256SUMS")" = old ] && grep -q "echo quarry" "$C/bin/quarry" \
  && ok "an incomplete runner set is not published; the forge deploys; the deploy ends FAILED stage=runner" || bad "partial: rc=$rc $(tail -1 "$C/out")"
setup partialdry; echo partial >"$C/build"; deploy --dry-run
[ "$rc" = 1 ] && journal 'FAILED stage=build .*runner' && ok "a dry run fails on an incomplete runner set" || bad "partialdry: rc=$rc"
setup full; deploy
[ "$rc" = 0 ] && journal '^OK ' && [ -f "$C/bin/runner/quarry-runner-linux-arm64" ] && grep -q "on-active=5" "$C/systemd-run" \
  && ok "a complete deploy publishes the set, schedules the runner restart and logs OK" || bad "full: rc=$rc $(tail -1 "$C/out")"
setup restart; echo 1 >"$C/systemd-run.rc"; deploy
[ "$rc" = 1 ] && journal 'FAILED stage=runner-restart ' && ! journal '^OK ' && ok "a runner restart that cannot be scheduled ends FAILED stage=runner-restart" || bad "restart: rc=$rc $(tail -1 "$C/out")"

# ---- the real zit --------------------------------------------------------
# Every zit call deploy.sh makes — pull from an upstream, open trunk from
# the store, info current — run against the binary itself in scratch
# stores, and the stub's fixtures held to what it prints. A zit whose
# output drifts fails here rather than on the host.
if ! command -v zit >/dev/null 2>&1; then
  echo "skip: no zit on PATH — the real-zit cases did not run"
else
  setup real
  # A COPY: the deploy installs the built zit over \$BIN/zit.
  cp "$(command -v zit)" "$C/bin/zit"
  (
    mkdir "$C/up" && cd "$C/up" && zit init -q && cp -R "$C/tree/." . &&
      zit add go.mod scripts/build-linux.sh scripts/runner-check.sh scripts/deploy.sh >/dev/null &&
      zit commit -m one --user t >/dev/null 2>&1
  ) || bad "real zit: could not build the scratch upstream"
  first=$(cd "$C/up" && zit info current | head -1)
  cp -R "$C/up" "$C/store"
  ( cd "$C/up" && echo two >>go.mod && zit commit -m two --user t >/dev/null 2>&1 )
  second=$(cd "$C/up" && zit info current | head -1)

  # The stub's fixtures, against the real output.
  line1=$(cd "$C/up" && zit info current | sed -n 1p)
  line2=$(cd "$C/up" && zit info current | sed -n 2p)
  bare1=$(cd "$C/up" && zit info | sed -n 1p)
  opened=$(mkdir "$C/o" && cd "$C/o" && zit open trunk --store="$C/up/.zit")
  case "$line1" in *[!0-9a-f]* | "") bad "real zit: info current line 1 is not a hash: $line1" ;; *)
    [ "${#line1}" = 64 ] && [ "$line2" = "branch:  trunk" ] && [ "$opened" = "opened $second on trunk" ] &&
      case "$bare1" in "repository:   "*) true ;; *) false ;; esac &&
      ok "the zit stub's fixtures match the real zit (info current, bare info, open)" ||
      bad "real zit output drifted from the stub: info current=[$line1 / $line2] info=[$bare1] open=[$opened]" ;;
  esac

  # An upstream to pull from: zit pull speaks only HTTP. The server
  # carries its own deadline — an alarm set before exec survives it — so
  # a harness killed part-way never leaves it serving.
  port=$((20000 + $$ % 20000))
  ( cd "$C/up" && exec perl -e 'alarm 120; exec @ARGV or die' zit serve --port "$port" ) >"$C/serve.log" 2>&1 &
  serve=$!; pids="$pids $serve"
  n=0; until curl -s -o /dev/null "http://127.0.0.1:$port/" || [ "$n" -ge 50 ]; do n=$((n + 1)); /bin/sleep 0.1; done

  export QUARRY_UPSTREAM="http://127.0.0.1:$port"
  deploy --expect "$second"
  unset QUARRY_UPSTREAM
  [ "$rc" = 0 ] && journal '^OK ' && [ "$(cd "$C/store" && zit info trunk | head -1)" = "$second" ] &&
    ok "real zit: pull from an upstream, open trunk, and a full --expect of the pulled tip deploy" ||
    bad "real zit deploy: rc=$rc $(tail -2 "$C/out" | tr '\n' ' ')"
  : >"$C/journal"
  # The deploy above installed its build's zit (a stub) over $BIN/zit.
  cp "$(command -v zit)" "$C/bin/zit"
  deploy --dry-run --expect "$(echo "$second" | cut -c1-12)"
  [ "$rc" = 0 ] && ok "real zit: a 12-character --expect of trunk's tip deploys" || bad "real zit 12-char: rc=$rc $(tail -1 "$C/out")"
  deploy --dry-run --expect "$first"
  [ "$rc" = 1 ] && journal 'FAILED stage=open .*trunk moved' && ok "real zit: a full --expect of an older check-in is refused" ||
    bad "real zit moved: rc=$rc $(tail -1 "$C/out")"
  kill "$serve" 2>/dev/null && wait "$serve" 2>/dev/null
fi

# ---- quarry-alert.sh ----------------------------------------------------
C=$T/alert; mkdir -p "$C"
stub curl 'printf "%s\n" "$@" >"$C/argv"; cat >"$C/stdin"; exit "$(cat "$C/curl.rc" 2>/dev/null || echo 0)"'
stub systemctl 'case "$*" in *Result*) echo exit-code ;; *InvocationID*) echo inv1 ;; esac'
stub journalctl 'echo "FAILED stage=ready tip=abc id=1-2 via=unit: not ready"'
alert() { PATH="$S:$PATH" QUARRY_ALERT_RUN="$C/run" QUARRY_NOTIFY_KEY=sekrit-key-123 sh "$here/quarry-alert.sh" "$@" >/dev/null 2>&1; rc=$?; }
alert --key k1 --message "hello"
if [ "$rc" = 0 ] && ! grep -q sekrit "$C/argv" && grep -q sekrit "$C/stdin"; then
  ok "the notify key reaches curl on stdin, never on its argv"
else
  bad "alert argv: rc=$rc argv=$(tr '\n' ' ' <"$C/argv")"
fi
echo 7 >"$C/curl.rc"; alert quarry
[ "$rc" = 1 ] && [ ! -s "$C/run/quarry.service" ] && ok "a failed send writes no quiet mark" || bad "mark after failed send: rc=$rc"
echo 0 >"$C/curl.rc"; : >"$C/argv"; alert quarry
[ "$rc" = 0 ] && [ -s "$C/argv" ] && [ -s "$C/run/quarry.service" ] && ok "the next failure sends, and marks" || bad "send after failed: rc=$rc"
: >"$C/argv"; alert quarry
[ "$rc" = 0 ] && [ ! -s "$C/argv" ] && ok "a failure inside the quiet period is not sent" || bad "quiet: rc=$rc"
echo garbage >"$C/run/quarry.service"; alert quarry
[ "$rc" = 0 ] && [ -s "$C/argv" ] && ok "a corrupt quiet mark reads as absent" || bad "corrupt mark: rc=$rc"
: >"$C/run/quarry.service"; : >"$C/argv"; alert quarry
[ "$rc" = 0 ] && [ -s "$C/argv" ] && ok "an empty quiet mark reads as absent" || bad "empty mark: rc=$rc"

# ---- quarry-deploy-watch.sh ---------------------------------------------
C=$T/watch; mkdir -p "$C"
stub journalctl 'cat "$C/journal" 2>/dev/null'
printf '#!/bin/sh\necho "$2" >>"%s/alerts"\n' "$C" >"$C/alert"; chmod +x "$C/alert"
watch() { PATH="$S:$PATH" QUARRY_DEPLOY_LOCK="$C/lock" QUARRY_DEPLOY_STATE="$C/state" QUARRY_DEPLOY_WATCH="$C/w" QUARRY_ALERT="$C/alert" sh "$here/quarry-deploy-watch.sh"; }
old=$(( $(date +%s) - 900 ))
# A live holder whose command line says deploy, as the real one's does.
sh -c '/bin/sleep 30' deploy.sh & pid=$!
echo "since then (pid $pid, id $old-$pid)" >"$C/lock"
watch; watch
kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null
[ "$(grep -c "deploy-long:$old-$pid" "$C/alerts" 2>/dev/null)" = 1 ] && ok "a deploy past ten minutes alerts once" || bad "long: $(cat "$C/alerts" 2>/dev/null)"
# The watcher must never take the lock to look: a deploy that starts
# while it looks would be refused. Hold nothing, and check a flock by
# the watcher would have been seen: the stub fails the watch if called.
stub flock 'echo "flock $*" >>"$C/flocked"; exit 1'
: >"$C/alerts"; watch
[ ! -f "$C/flocked" ] && ok "the watcher never takes the deploy lock" || bad "watcher called flock: $(cat "$C/flocked")"
echo "since then (pid $pid, id $old-$pid)" >"$C/lock"
echo "$old-$pid rollback abc" >"$C/state"; : >"$C/journal"; watch; watch
[ "$(grep -c "deploy-lost:$old-$pid" "$C/alerts")" = 1 ] && ok "a deploy that died mid-rollback with no result line alerts once" || bad "lost: $(cat "$C/alerts")"

if [ "$fails" -ne 0 ]; then echo "$fails FAILED"; exit 1; fi
echo "all ok"