Skip to content
quarry Artifact [92c65f0905]
Login

Artifact [92c65f0905]

Artifact 92c65f090525332ab35fb5d53848ba76e23ec2d82dff6f60d91587f553e28fbe:


#!/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,SC2012
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 'echo "$*" >>"$C/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, and so
# do its unversioned files: releases/X.Y.Z/FILE is $C/rel/X.Y.Z/FILE.
stub curl 'url=; out=; prev=; for a; do [ "$prev" = -o ] && out=$a; case "$a" in http*) url=$a ;; esac; prev=$a; done
echo "$url" >>"$C/fetched"
body() {
  case "$url" in
    */api/v1/r/*/checkin/*) [ -f "$C/checkin.json" ] || exit 22; cat "$C/checkin.json" ;;
    */api/v1/version) echo "{\"ready\":true}" ;;
    */r/store/uv/releases/*) f=$C/rel/${url##*/uv/releases/}; [ -f "$f" ] || exit 22; cat "$f" ;;
    # $C/down: the forge stays silent while the 0.2.0 release is installed.
    http://127.0.0.1:8950/) [ -f "$C/down" ] && grep -q "quarry 0.2.0" "$C/bin/quarry" && exit 7; echo "<p>quarry 0.1.0</p>" ;;
  esac
}
if [ -n "$out" ]; then body >"$out"; else body; fi
exit 0'
# tar: every unpack is recorded, so a case can prove nothing was
# unpacked before the sums were verified.
realtar=$(command -v tar)
stub tar 'echo "$*" >>"$C/untarred"; exec '"$realtar"' "$@"'
# 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" = -e ] && case "$a" in VERSION=*) echo "${a#VERSION=}" >"$C/stamp" ;; 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"
  printf '# Changelog\n\n## 0.2.0 (2026-10-03)\n\n- A release.\n' >"$C/tree/CHANGELOG.md"
  # The repository's release names (zit tag list): 0.2.0 is out.
  echo version-0.2.0 >"$C/released"
  : >"$C/tags"
  echo version-0.2.0 >"$C/reltag"
  echo trunk >"$C/relbranch"
  # The store's timeline, as zit timeline -F medium prints it: one
  # check-in tagged version-0.2.0, on trunk.
  printf '%s\n' "=== Thu 2026-10-01 ===" "17:00:37 [$(echo "$H" | cut -c1-12)] two (user: t tags: trunk, version-0.2.0)" \
    "17:00:30 [$(echo "$OTHER" | cut -c1-12)] one (user: t tags: trunk)" >"$C/timeline"
  # trunk's history from its tip, as zit log -n 0 trunk prints it.
  printf 'checkin %s\n' "$H" "$OTHER" >"$C/history"
  cp "$here/build-linux.sh" "$here/runner-check.sh" "$here/deploy.sh" "$here/version-stamp.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" ;;
  "tag list --raw \$h")
    [ ! -f "$C/tagfail" ] || { echo "zit: tag list failed" >&2; exit 1; }
    cat "$C/tags" ;;
  "tag list --raw "*)
    cat "$C/ptags" 2>/dev/null || true ;;
  "tag list \$h")
    cat "$C/plain" 2>/dev/null || true ;;
  "tag list")
    cat "$C/released" ;;
  "timeline -R $C/store/.zit -n 0 -F medium")
    cat "$C/timeline" ;;
  "log -R $C/store/.zit -n 0 trunk")
    cat "$C/history" ;;
  "info -R $C/store/.zit tag:"*)
    [ "\${4#tag:}" = "\$(cat "$C/reltag")" ] || { echo "zit: no such check-in: \$4" >&2; exit 1; }
    echo "\$h"
    echo "branch:  \$(cat "$C/relbranch")" ;;
  "info \$h")
    echo "\$h"
    echo "branch:  trunk"
    echo "leaf:    open"
    if [ -f "$C/parent" ]; then echo "parent:  \$(cat "$C/parent")"; fi ;;
  "cat -R $C/store/.zit scripts/"*" tag:"*)
    set -- \$*
    [ "\$5" = "tag:\$(cat "$C/reltag")" ] || { echo "zit: no such check-in: \$5" >&2; exit 1; }
    cat "$C/tree/\$4" ;;
  "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 CHANGELOG.md scripts/build-linux.sh scripts/runner-check.sh scripts/deploy.sh scripts/version-stamp.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 stamp's reads: the raw tags of a check-in, inherited ones
  # included, and its parent.
  ( cd "$C/up" && zit tag add --propagate version-0.1.0 "$first" >/dev/null 2>&1 && zit tag add version-0.2.0 "$second" >/dev/null 2>&1 )
  rawtags=$(cd "$C/up" && zit tag list --raw "$second" | tr '\n' ' ')
  parent=$(cd "$C/up" && zit info "$second" | sed -n 's/^parent: *//p')
  tl=$(cd "$C/up" && zit timeline -R "$C/up/.zit" -n 0 -F medium | grep -F "[$(echo "$second" | cut -c1-12)]")
  on=$(zit info -R "$C/up/.zit" tag:version-0.2.0 | sed -n 's/^branch: *//p')
  plain=$(cd "$C/up" && zit tag list "$second" | tr '\n' ' ')
  hist=$(zit log -R "$C/up/.zit" -n 0 trunk | tr '\n' ' ')
  [ "$hist" = "checkin $second checkin $first " ] || bad "real zit log drifted: [$hist]"
  # A propagated tag shows on its carrier only, so a release tag counts once.
  case "$tl" in *"tags: trunk, version-0.2.0)") true ;; *) bad "real zit timeline line drifted: [$tl]" ;; esac
  [ "$on" = trunk ] && [ "$plain" = "version-0.2.0 " ] || bad "real zit info -R / tag list drifted: branch=[$on] plain=[$plain]"
  [ "$rawtags" = "branch trunk sym-trunk sym-version-0.1.0 sym-version-0.2.0 " ] && [ "$parent" = "$first" ] &&
    [ "$(cd "$C/up" && zit cat -R "$C/up/.zit" CHANGELOG.md tag:version-0.2.0 | sed -n 3p)" = "## 0.2.0 (2026-10-03)" ] &&
    [ "$(cd "$C/up" && zit tag list)" = version-0.2.0 ] &&
    ok "the zit stub's fixtures match the real zit (tag list --raw, tag list, info CHECKIN, cat -R at a tag)" ||
    bad "real zit drifted from the stub: tag list --raw=[$rawtags] parent=[$parent]"
  # 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

# ---- the version stamp --------------------------------------------------
# version-stamp.sh is the rule; the deploy passes the tree's answer to the
# build as VERSION. The changelog's newest heading is the release the tree
# is at or heading for; once it is out, the next patch is.
V=$T/CHANGELOG.md; printf '# Changelog\n\n## 0.2.0 (2026-10-03)\n\n## 0.1.0-beta.1\n' >"$V"
h12=$(echo "$H" | cut -c1-12)
stamp() { sh "$here/version-stamp.sh" "$@" 2>/dev/null; }
[ "$(stamp "$V" "$H" release version-0.2.0)" = 0.2.0 ] && ok "a check-in tagged version-X.Y.Z stamps X.Y.Z" || bad "tagged stamp: $(stamp "$V" "$H" release version-0.2.0)"
[ "$(stamp "$V" "$H" version-0.1.0 release)" = "0.2.1-dev+$h12" ] && ok "another version's tag stamps the next patch, -dev+<check-in>" || bad "other tag stamp: $(stamp "$V" "$H" version-0.1.0)"
[ "$(stamp "$V" "$h12")" = "0.2.1-dev+$h12" ] && ok "an untagged check-in stamps the next patch, -dev+<check-in>" || bad "untagged stamp"
[ "$(stamp "$V" local)" = "0.2.1-dev+local" ] && ok "a tree with no checkout stamps the next patch, -dev+local" || bad "local stamp"
printf '# Changelog\n\n## Unreleased\n\n## 0.1.0-beta.1\n' >"$T/CHANGELOG.bad"
! stamp "$T/CHANGELOG.bad" "$H" >/dev/null && ! stamp "$V" 0123 >/dev/null && ! stamp "$T/nothing.md" "$H" >/dev/null &&
  ok "a changelog with no X.Y.Z heading, a missing one, or a short check-in is refused" || bad "bad stamp inputs accepted"
raw() { printf '%s\n' "$@"; }
[ "$({ raw 'branch trunk' sym-trunk sym-version-0.2.0; echo --; raw 'branch trunk' sym-trunk; echo --; } | stamp "$V" "$H" -)" = 0.2.0 ] &&
  ok "a tag the check-in carries itself stamps X.Y.Z, read from zit tag list --raw" || bad "raw carried stamp"
[ "$({ raw 'branch trunk' sym-trunk sym-version-0.2.0; echo --; raw 'branch trunk' sym-trunk sym-version-0.2.0; echo --; } | stamp "$V" "$H" -)" = "0.2.1-dev+$h12" ] &&
  ok "a propagating version tag inherited from the parent does not stamp the release" || bad "raw inherited stamp"
[ "$({ raw 'branch trunk' sym-version-0.2.0; echo --; raw 'branch trunk' sym-version-0.2.0; echo --; raw version-0.2.0; } | stamp "$V" "$H" -)" = 0.2.0 ] &&
  ok "a check-in tagged with a name its parent also carries stamps the release, as the trigger fires" || bad "raw re-tag stamp"
[ "$({ raw 'branch version-0.2.0' sym-version-0.2.0; echo --; echo --; } | stamp "$V" "$H" -)" = "0.2.1-dev+$h12" ] &&
  ok "a branch's own name is not a version tag" || bad "raw branch stamp"
[ "$({ raw 'branch trunk'; echo --; echo --; echo --; raw version-0.1.0 version-0.2.0; } | stamp "$V" "$H" -)" = "0.2.1-dev+$h12" ] &&
  ok "once the newest heading is released, untagged check-ins head for the next patch" || bad "released stamp"
[ "$({ raw 'branch trunk'; echo --; echo --; echo --; raw version-0.1.0; } | stamp "$V" "$H" -)" = "0.2.0-dev+$h12" ] &&
  ok "a heading not yet released is the release untagged check-ins head for" || bad "unreleased stamp"
setup st_rel; printf 'branch trunk\nsym-trunk\nsym-release\nsym-version-0.2.0\n' >"$C/tags"; deploy --dry-run
[ "$rc" = 0 ] && [ "$(cat "$C/stamp")" = 0.2.0 ] && ok "a deploy of a tip tagged version-0.2.0 builds 0.2.0" || bad "deploy stamp tagged: rc=$rc stamp=$(cat "$C/stamp" 2>/dev/null)"
setup st_inh; echo "$OTHER" >"$C/parent"; printf 'branch trunk\nsym-trunk\nsym-version-0.2.0\n' | tee "$C/tags" >"$C/ptags"; deploy --dry-run
[ "$rc" = 0 ] && [ "$(cat "$C/stamp")" = "0.2.1-dev+$(echo "$H" | cut -c1-12)" ] && ok "a deploy of a tip that only inherits version-0.2.0 builds the next patch, -dev+<tip>" || bad "deploy stamp inherited: rc=$rc stamp=$(cat "$C/stamp" 2>/dev/null)"
setup st_fail; printf 'sym-version-0.2.0\n' >"$C/tags"; touch "$C/tagfail"; deploy
[ "$rc" = 0 ] && journal '^OK ' && [ "$(cat "$C/stamp")" = "0.2.1-dev+$(echo "$H" | cut -c1-12)" ] && grep -q "WARNING .*tags" "$C/out" \
  && ok "a trunk deploy whose tag lookup fails still deploys, stamped -dev, with a warning" || bad "deploy stamp tagfail: rc=$rc stamp=$(cat "$C/stamp" 2>/dev/null) $(tail -1 "$C/out")"
setup st_pfail; echo "$OTHER" >"$C/parent"; printf 'sym-version-0.2.0\n' >"$C/tags"; rm -f "$C/ptags"; deploy --dry-run
[ "$rc" = 0 ] && [ "$(cat "$C/stamp")" = 0.2.0 ] && ok "a tip whose parent carries no tags stamps its own version tag" || bad "deploy stamp parent: rc=$rc stamp=$(cat "$C/stamp" 2>/dev/null)"
setup st_dev; deploy --dry-run
[ "$rc" = 0 ] && [ "$(cat "$C/stamp")" = "0.2.1-dev+$(echo "$H" | cut -c1-12)" ] && ok "a deploy of an untagged tip after 0.2.0 builds 0.2.1-dev+<tip>" || bad "deploy stamp dev: rc=$rc stamp=$(cat "$C/stamp" 2>/dev/null)"
setup st_unrel; echo version-0.1.0 >"$C/released"; deploy --dry-run
[ "$rc" = 0 ] && [ "$(cat "$C/stamp")" = "0.2.0-dev+$(echo "$H" | cut -c1-12)" ] && ok "a deploy before the newest heading is tagged builds that release's -dev" || bad "deploy stamp unreleased: rc=$rc stamp=$(cat "$C/stamp" 2>/dev/null)"
setup st_old; rm "$C/tree/CHANGELOG.md"; deploy --dry-run
[ "$rc" = 0 ] && [ "$(cat "$C/stamp")" = "0.1.0-beta+$(echo "$H" | cut -c1-12)" ] && ok "a tree from before versions keeps its old stamp" || bad "deploy stamp old: rc=$rc stamp=$(cat "$C/stamp" 2>/dev/null)"

# ---- --release and --rollback ------------------------------------------
# A release as RFC 0020 lays it out: per arch a tarball holding quarry and
# zit in one top directory, the five runner downloads, one SHA256SUMS.
mkrelease() {
  v=$1; R=$C/rel/$v; mkdir -p "$R"
  for arch in amd64 arm64; do
    d=$C/pkg/quarry-$v-linux-$arch; mkdir -p "$d"
    printf '#!/bin/sh\necho "quarry %s"\n' "${2:-$v}" >"$d/quarry"
    printf '#!/bin/sh\necho "zit from %s"\n' "$v" >"$d/zit"
    chmod +x "$d/quarry" "$d/zit"; echo log >"$d/CHANGELOG.md"; echo l >"$d/LICENSE"
    tar -czf "$R/quarry-$v-linux-$arch.tar.gz" -C "$C/pkg" "quarry-$v-linux-$arch"
  done
  for f in linux-amd64 linux-arm64 darwin-arm64 darwin-amd64 windows-amd64.exe; do echo "runner $v $f" >"$R/quarry-runner-$f"; done
  ( cd "$R" && sha256sum ./* | sed 's|  \./|  |' >SHA256SUMS )
}
installed_old() { grep -q "echo old" "$C/bin/quarry" && [ "$(cat "$C/bin/runner/SHA256SUMS")" = old ] && [ ! -f "$C/systemctl" ]; }
for flags in "--release 0.2.0 --require-green" "--release 0.2.0 --expect $H" "--release 0.2" "--release" "--rollback --release 0.2.0" "--rollback --dry-run"; do
  setup rflags
  # shellcheck disable=SC2086
  deploy $flags
  [ "$rc" = 2 ] && ok "deploy.sh $flags is a usage error" || bad "flags $flags: rc=$rc"
done
setup rel_bad; mkrelease 0.2.0; echo tampered >>"$C/rel/0.2.0/quarry-runner-darwin-arm64"; deploy --release 0.2.0
[ "$rc" = 1 ] && journal 'FAILED stage=verify .*SHA256SUMS' && installed_old && [ ! -e "$C/untarred" ] \
  && ok "a release whose sums do not verify stops before unpacking or installing anything" || bad "rel_bad: rc=$rc $(tail -1 "$C/out")"
setup rel_none; echo version-0.9.0 >"$C/reltag"; sed -i.x 's/version-0.2.0/version-0.9.0/' "$C/timeline"; deploy --release 0.9.0
[ "$rc" = 1 ] && journal 'FAILED stage=fetch ' && installed_old && ok "a release tagged in the store that the forge does not have is refused" || bad "rel_none: rc=$rc"
setup rel_name; mkrelease 0.2.0; echo "$H  ../escape" >>"$C/rel/0.2.0/SHA256SUMS"; deploy --release 0.2.0
[ "$rc" = 1 ] && journal 'FAILED stage=fetch .*file name' && installed_old && ok "a SHA256SUMS naming a path outside the release is refused" || bad "rel_name: rc=$rc $(tail -1 "$C/out")"
setup rel_wrong; mkrelease 0.2.0 0.1.9; deploy --release 0.2.0
[ "$rc" = 1 ] && journal 'FAILED stage=verify .*not quarry 0.2.0' && installed_old && ok "a release whose quarry names another version is refused" || bad "rel_wrong: rc=$rc $(tail -1 "$C/out")"
setup rel_runners; mkrelease 0.2.0; rm "$C/rel/0.2.0/quarry-runner-linux-arm64" "$C/rel/0.2.0/quarry-runner-darwin-amd64"
grep -v 'quarry-runner-linux-arm64\|quarry-runner-darwin-amd64' "$C/rel/0.2.0/SHA256SUMS" >"$C/s" && mv "$C/s" "$C/rel/0.2.0/SHA256SUMS"; deploy --release 0.2.0
[ "$rc" = 1 ] && journal 'FAILED stage=verify .*runner.*quarry-runner-linux-arm64' && installed_old \
  && ok "a release listing 3 of the 5 runners its tree builds is refused, installing nothing" || bad "rel_runners: rc=$rc $(tail -1 "$C/out")"
setup rel_untagged; mkrelease 0.2.0; echo version-0.1.0 >"$C/reltag"; sed -i.x 's/, version-0.2.0//' "$C/timeline"; deploy --release 0.2.0
[ "$rc" = 1 ] && journal 'FAILED stage=tag .*no check-in tagged version-0.2.0: pull the store' && installed_old && [ ! -e "$C/fetched" ] \
  && ok "a release the store has no tag for is refused before fetching, saying to pull the store" || bad "rel_untagged: rc=$rc $(tail -1 "$C/out")"
setup rel_branch; mkrelease 0.2.0; echo feat >"$C/relbranch"; deploy --release 0.2.0
[ "$rc" = 1 ] && journal 'FAILED stage=tag .*on feat, not trunk' && installed_old && [ ! -e "$C/fetched" ] \
  && ok "a release tag on a check-in off trunk is refused, running nothing from it" || bad "rel_branch: rc=$rc $(tail -1 "$C/out")"
setup rel_fork; mkrelease 0.2.0; echo "checkin $OTHER" >"$C/history"; deploy --release 0.2.0
[ "$rc" = 1 ] && journal "FAILED stage=tag .*labelled trunk but is not in trunk's history" && installed_old && [ ! -e "$C/fetched" ] \
  && ok "a release tag on a fork leaf labelled trunk but outside trunk's history is refused" || bad "rel_fork: rc=$rc $(tail -1 "$C/out")"
setup rel_dots; mkrelease 0.2.0; echo "17:00:20 [aaaaaaaaaaaa] x (user: t tags: feat, version-0x2x0)" >>"$C/timeline"; deploy --dry-run --release 0.2.0
[ "$rc" = 0 ] && ok "a tag name that matches version-X.Y.Z only as a regex is not counted as a carrier" || bad "rel_dots: rc=$rc $(tail -1 "$C/out")"
# The store's runner check runs as quarry on a copy: one that rewrites
# every file it is given, and every file beside them, changes nothing
# that is installed or served.
setup rel_evil; mkrelease 0.2.0
cat >"$C/tree/scripts/runner-check.sh" <<'EVIL'
for d in "$1" "$1/.." "$1/../.." "$(dirname "$2")"; do
  for f in "$d"/* "$d"/bin/* "$d"/bin/runner/*; do [ -f "$f" ] && ! [ "$f" -ef "$0" ] && echo evil >"$f" 2>/dev/null; done
done
exit 0
EVIL
deploy --release 0.2.0
if [ "$rc" = 0 ] && [ "$("$C/bin/quarry" version)" = "quarry 0.2.0" ] && [ "$("$C/bin/zit")" = "zit from 0.2.0" ] \
  && (cd "$C/bin/runner" && sha256sum -c --quiet SHA256SUMS >/dev/null 2>&1) && ! grep -rq evil "$C/bin/runner"; then
  ok "a store runner check that rewrites what it can reach changes nothing installed"
else
  bad "rel_evil: rc=$rc $(tail -1 "$C/out")"
fi
setup rel_twice; mkrelease 0.2.0; sed -i.x 's/tags: trunk)$/tags: feat, version-0.2.0)/' "$C/timeline"; deploy --release 0.2.0
[ "$rc" = 1 ] && journal 'FAILED stage=tag .*2 check-ins carry version-0.2.0' && installed_old \
  && ok "a release tag carried by two check-ins is refused as ambiguous" || bad "rel_twice: rc=$rc $(tail -1 "$C/out")"
setup rel_dirs; mkrelease 0.2.0; for arch in amd64 arm64; do mkdir -p "$C/pkg/extra"; echo x >"$C/pkg/extra/quarry"
  tar -czf "$C/rel/0.2.0/quarry-0.2.0-linux-$arch.tar.gz" -C "$C/pkg" extra "quarry-0.2.0-linux-$arch"; done
( cd "$C/rel/0.2.0" && rm SHA256SUMS && sha256sum ./* | sed 's|  \./|  |' >SHA256SUMS ); deploy --release 0.2.0
[ "$rc" = 1 ] && journal 'FAILED stage=verify .*layout' && installed_old \
  && ok "a release tarball that is not one quarry-X.Y.Z-linux-<arch>/ directory is refused" || bad "rel_dirs: rc=$rc $(tail -1 "$C/out")"
setup rel_dry; mkrelease 0.2.0; deploy --dry-run --release 0.2.0
[ "$rc" = 0 ] && installed_old && grep -q 'releases/0.2.0/quarry-runner-windows-amd64.exe' "$C/fetched" \
  && ok "a dry-run release fetches and verifies, installing nothing" || bad "rel_dry: rc=$rc $(tail -1 "$C/out")"
setup rel; mkrelease 0.2.0; deploy --release 0.2.0
n=$(grep -c 'quarry-runner-' "$C/bin/runner/SHA256SUMS" 2>/dev/null)
if [ "$rc" = 0 ] && journal '^OK .*tip=0.2.0' && [ "$("$C/bin/quarry" version)" = "quarry 0.2.0" ] && grep -q "echo old" "$C/bin/quarry.prev" \
  && [ "$("$C/bin/zit")" = "zit from 0.2.0" ] && grep -q "zit stub" "$C/bin/zit.prev" && [ "$n" = 5 ] && [ "$(find "$C/bin/runner" -name "quarry-runner-*" | wc -l | tr -d " ")" = 5 ] \
  && (cd "$C/bin/runner" && sha256sum -c --quiet SHA256SUMS >/dev/null 2>&1) && [ "$(cat "$C/bin/runner.prev/SHA256SUMS")" = old ] \
  && [ ! -f "$C/stamp" ] && grep -q "restart quarry.service" "$C/systemctl" && grep -q "on-active=5" "$C/systemd-run" \
  && grep -q '^-u quarry sh .*/runner-check.sh ' "$C/sudo"; then
  ok "a good release installs quarry, its zit and the whole runner set, keeps quarry.prev and zit.prev, builds nothing, runs the store's runner check as quarry, restarts the runner last"
else
  bad "rel: rc=$rc runners=$n $(tail -1 "$C/out")"
fi
: >"$C/journal"; rm -f "$C/systemctl"; deploy --rollback
[ "$rc" = 0 ] && journal '^OK ' && grep -q "echo old" "$C/bin/quarry" && [ ! -e "$C/bin/quarry.prev" ] \
  && grep -q "zit stub" "$C/bin/zit" && [ ! -e "$C/bin/zit.prev" ] \
  && [ "$(cat "$C/bin/runner/SHA256SUMS")" = old ] && grep -q "restart quarry.service" "$C/systemctl" \
  && ok "--rollback restores quarry.prev, zit.prev and the previous runner set, and restarts" || bad "rollback: rc=$rc $(tail -1 "$C/out")"
: >"$C/journal"; deploy --rollback
[ "$rc" = 1 ] && journal 'FAILED stage=reinstate .*no .*quarry.prev' && grep -q "echo old" "$C/bin/quarry" \
  && ok "--rollback with no quarry.prev changes nothing and says so" || bad "rollback twice: rc=$rc $(tail -1 "$C/out")"

setup rel_silent; mkrelease 0.2.0; touch "$C/down"; deploy --release 0.2.0
[ "$rc" = 1 ] && journal 'FAILED stage=health .*rolled back' && grep -q "echo old" "$C/bin/quarry" && grep -q "zit stub" "$C/bin/zit" \
  && [ "$(cat "$C/bin/runner/SHA256SUMS")" = old ] \
  && ok "a release that does not answer is rolled back, its zit and runner set with it" || bad "rel_silent: rc=$rc $(tail -1 "$C/out")"

# The .prev trio is always one deploy old together: a deploy whose
# runner set is not published still moves runner.prev to the set that
# went with the quarry it keeps as quarry.prev.
setup trio; cp "$C/bin/zit" "$C/zit.stub"; deploy; first=$(cat "$C/bin/runner/SHA256SUMS")
# The deploy installed its build's zit over the stub; the next one opens trunk with the stub.
cp "$C/zit.stub" "$C/bin/zit"; echo partial >"$C/build"; : >"$C/journal"; deploy
[ "$rc" = 1 ] && [ "$(cat "$C/bin/runner.prev/SHA256SUMS" 2>/dev/null)" = "$first" ] \
  && ok "a deploy that keeps the runner set keeps it as runner.prev too" || bad "trio: rc=$rc runner.prev=$(cat "$C/bin/runner.prev/SHA256SUMS" 2>/dev/null | head -1)"
: >"$C/journal"; deploy --rollback
[ "$rc" = 0 ] && [ "$(cat "$C/bin/runner/SHA256SUMS")" = "$first" ] \
  && ok "--rollback after it restores the runner set that went with quarry.prev" || bad "trio rollback: rc=$rc"

# ---- 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 ---------------------------------------------
# Who holds the lock is read from a /proc/locks laid out as the kernel
# prints it (a FLOCK line keyed MAJOR:MINOR:INODE in hex), so this runs
# on macOS too; `stat -c '%d %i'` is stubbed with the lock file's real
# inode on device 2049 (08:01), which is what that line names.
C=$T/watch; mkdir -p "$C"
stub journalctl 'cat "$C/journal" 2>/dev/null'
stub stat '[ "$1 $2 $3" = "-L -c %d %i" ] || exit 1; echo "2049 $(ls -di "$4" | awk "{print \$1}")"'
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" QUARRY_PROC_LOCKS="$C/locks" sh "$here/quarry-deploy-watch.sh"; }
old=$(( $(date +%s) - 900 ))
echo "since then (pid 4242, id $old-4242)" >"$C/lock"
ino=$(ls -di "$C/lock" | awk '{print $1}')
held_line="7: FLOCK  ADVISORY  WRITE 4242 08:01:$ino 0 EOF"

# Held by the kernel's account: a deploy past ten minutes, alerted once.
printf '%s\n' "1: OFDLCK ADVISORY  READ -1 08:01:5045820 128 128" "$held_line" >"$C/locks"
watch; watch
[ "$(grep -c "deploy-long:$old-4242" "$C/alerts" 2>/dev/null)" = 1 ] && ok "a lock /proc/locks shows held past ten minutes alerts once" || bad "long: $(cat "$C/alerts" 2>/dev/null)"

# A stale note naming a LIVE process whose command line says deploy —
# a reused pid, or this very harness — is not a holder: only the kernel's
# lock table says who holds the lock. With the state mid-rollback, that
# is a deploy that died without a result.
sh -c '/bin/sleep 30' deploy.sh & live=$!
echo "since then (pid $live, id $old-$live)" >"$C/lock"
printf '%s\n' "1: OFDLCK ADVISORY  READ -1 08:01:5045820 128 128" "2: FLOCK  ADVISORY  WRITE 1 08:01:999999999 0 EOF" >"$C/locks"
echo "$old-$live rollback abc" >"$C/state"; : >"$C/journal"; : >"$C/alerts"; watch; watch
kill "$live" 2>/dev/null; wait "$live" 2>/dev/null
if [ "$(grep -c "deploy-lost:$old-$live" "$C/alerts")" = 1 ] && ! grep -q deploy-long "$C/alerts"; then
  ok "a live pid in a stale lock note is not a holder; the dead deploy alerts once"
else
  bad "stale pid: $(cat "$C/alerts")"
fi

# A waiter holds nothing.
echo "$old-4243 ok abc" >"$C/state"; : >"$C/alerts"
echo "3: -> FLOCK  ADVISORY  WRITE 4243 08:01:$ino 0 EOF" >"$C/locks"; watch
[ ! -s "$C/alerts" ] && ok "a waiter's line in /proc/locks is not a holder" || bad "waiter: $(cat "$C/alerts")"

# No /proc/locks: no holder, never an alert about one.
rm -f "$C/locks"; : >"$C/alerts"; watch
[ ! -s "$C/alerts" ] && ok "without /proc/locks there is no holder" || bad "no locks: $(cat "$C/alerts")"

# The watcher never takes the lock to look: a deploy that starts while
# it looks would be refused.
stub flock 'echo "flock $*" >>"$C/flocked"; exit 1'
printf '%s\n' "$held_line" >"$C/locks"; watch
[ ! -f "$C/flocked" ] && ok "the watcher never takes the deploy lock" || bad "watcher called flock: $(cat "$C/flocked")"

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