#!/usr/bin/env bash # test.sh (e.g. ~/src/posts) # # Runs release end to end in a throwaway sandbox: a local bare remote # instead of Gogs, and stand-in sudo/systemctl that record what they were # asked to do. Nothing on the real server is touched. set -euo pipefail here=$(cd "$(dirname "$0")" && pwd) src=$(cd "${1:?usage: test.sh }" && pwd) T=$(mktemp -d); trap 'rm -rf "$T"' EXIT pass=0; fail=0 ok() { echo "ok $*"; pass=$((pass+1)); } bad() { echo "FAIL $*"; fail=$((fail+1)); } check() { local what=$1; shift; if "$@"; then ok "$what"; else bad "$what"; fi; } # Remote: bajdev/posts (go app with content) and bajdev/site (static). mkdir -p "$T/remote/bajdev" "$T/apps/posts/content" "$T/vhosts/site.example" "$T/bin" git clone --quiet --bare "$src" "$T/remote/bajdev/posts.git" # Start from no deploy tags, whatever the source repo already has. git -C "$T/remote/bajdev/posts.git" tag -l 'deploy/*' | xargs -r git -C "$T/remote/bajdev/posts.git" tag -d >/dev/null git init --quiet -b main "$T/site" && mkdir "$T/site/public" && echo hi > "$T/site/public/index.html" git -C "$T/site" add . && git -C "$T/site" -c user.name=t -c user.email=t@t commit --quiet -m init git clone --quiet --bare "$T/site" "$T/remote/bajdev/site.git" echo stale > "$T/apps/posts/content/removed-post.md" cat > "$T/sites.json" < "$T/bin/sudo" printf '#!/bin/sh\necho "$*" >> "%s/systemctl.log"\n[ "$1" = is-active ] && [ -n "$SYSTEMCTL_FAIL" ] && exit 3\nexit 0\n' "$T" > "$T/bin/systemctl" chmod +x "$T/bin/sudo" "$T/bin/systemctl" export PATH="$T/bin:$PATH" RELEASE_CONF="$T/sites.json" RELEASE_GIT_BASE="$T/remote/" \ RELEASE_WORK="$T/work" RELEASE_APPS="$T/apps" RELEASE_VHOSTS="$T/vhosts" RELEASE_LOG="$T/deploys.log" \ GIT_AUTHOR_NAME=deploy GIT_AUTHOR_EMAIL=deploy@test GIT_COMMITTER_NAME=deploy GIT_COMMITTER_EMAIL=deploy@test R="$here/release" head=$(git -C "$src" rev-parse main) # 1. A normal release of the Go app. out=$(DEPLOY_BY="PR merged by bajdev" "$R" bajdev/posts "$head" 2>&1) || { bad "release posts: $out"; } check "binary built" test -x "$T/apps/posts/posts" check "no .new left behind" test ! -e "$T/apps/posts/posts.new" check "content synced" test -f "$T/apps/posts/content/2026-10-24-hello-world.md" check "deleted posts removed" test ! -e "$T/apps/posts/content/removed-post.md" check "service restarted" grep -qx "restart goapp@posts" "$T/systemctl.log" tag1=$(git -C "$T/remote/bajdev/posts.git" tag -l 'deploy/posts/*' | tail -1) check "deploy tag pushed" test -n "$tag1" check "tag is on the commit" test "$(git -C "$T/remote/bajdev/posts.git" rev-parse "$tag1^{commit}")" = "$head" check "log line written" grep -q "posts $head $tag1 PR merged by bajdev" "$T/deploys.log" # 2. Rollback to that tag (a second later, so the new tag name differs). sleep 1 out=$(DEPLOY_BY="rollback by bajdev" "$R" bajdev/posts "$tag1" 2>&1) || bad "rollback: $out" check "rollback logged" grep -q "rollback by bajdev" "$T/deploys.log" check "rollback tagged" test "$(git -C "$T/remote/bajdev/posts.git" tag -l 'deploy/posts/*' | wc -l)" -eq 2 # 3. Static site. out=$("$R" bajdev/site main 2>&1) || bad "release site: $out" check "static files synced" test -f "$T/vhosts/site.example/index.html" check "static tag pushed" test -n "$(git -C "$T/remote/bajdev/site.git" tag -l 'deploy/site.example/*')" # 4. Refusals. refused() { # local want=$1; shift; local out if out=$("$R" "$@" 2>&1); then return 1; fi [[ $out == *"$want"* ]] } check "unknown repo refused" refused "unknown repo" bajdev/nope main check "../ repo name refused" refused "bad repo name" ../x main check "dot-leading name refused" refused "bad repo name" bajdev/.git main check "option-like ref refused" refused "bad ref" bajdev/posts --upload-pack=x check "wrong arg count refused" refused "usage:" bajdev/posts # 5. A service that fails to start: no tag, no log line. tags_before=$(git -C "$T/remote/bajdev/posts.git" tag -l 'deploy/posts/*' | wc -l) lines_before=$(wc -l < "$T/deploys.log") sleep 1 check "failed start exits non-zero" bash -c '! SYSTEMCTL_FAIL=1 "$0" bajdev/posts main >/dev/null 2>&1' "$R" check "failed start not tagged" test "$(git -C "$T/remote/bajdev/posts.git" tag -l 'deploy/posts/*' | wc -l)" -eq "$tags_before" check "failed start not logged" test "$(wc -l < "$T/deploys.log")" -eq "$lines_before" # 6. What deployd sends: origin/main, after main has moved on since the work # clone was made (another PR merged meanwhile). The release must include it. up=$T/upstream git clone --quiet "$T/remote/bajdev/posts.git" "$up" printf -- '---\ntitle: Merged by another PR\n---\n\nHi.\n' > "$up/content/2026-12-01-other-pr.md" git -C "$up" add content && git -C "$up" commit --quiet -m "Another PR" && git -C "$up" push --quiet origin HEAD:main tip=$(git -C "$T/remote/bajdev/posts.git" rev-parse main) sleep 1 out=$(DEPLOY_BY="PR merged by bajdev (head 0bfb14e)" "$R" bajdev/posts origin/main 2>&1) || bad "release origin/main: $out" check "origin/main includes the other PR" test -f "$T/apps/posts/content/2026-12-01-other-pr.md" tagN=$(git -C "$T/remote/bajdev/posts.git" tag -l 'deploy/posts/*' | tail -1) check "tag is on main's new tip" test "$(git -C "$T/remote/bajdev/posts.git" rev-parse "$tagN^{commit}")" = "$tip" check "log records main's tip" grep -q "posts $tip $tagN PR merged by bajdev (head 0bfb14e)" "$T/deploys.log" echo "$pass passed, $fail failed" [[ $fail -eq 0 ]]