#!/usr/bin/env bash
# Build the site and publish it to the `pages` branch.
#
#   .agents/scripts/publish-pages.sh              # publish
#   .agents/scripts/publish-pages.sh --dry-run    # build and report, push nothing
#   .agents/scripts/publish-pages.sh --pending X  # main is *about* to become X
#
# `--pending` exists for the pre-push hook. Git has no post-push hook, so the
# hook necessarily runs before main lands on the remote and the usual "HEAD must
# equal origin/main" check cannot hold yet. The caller asserts the SHA the push
# will create, and the hook only asserts it after confirming the push is a
# fast-forward.
#
# `pages` is what the Gitea Pages Server actually serves. Publishing overwrites
# the live site. There is no staging environment between here and visitors.
#
# This never checks `pages` out. It writes a tree straight from `dist/` with
# plumbing (`write-tree` + `commit-tree`), so your working tree is untouched and
# a failure halfway through leaves nothing behind. The commit is parented on the
# current `pages`, so the branch keeps its history and rollback is one push.
set -euo pipefail

cd "$(git rev-parse --show-toplevel)"

dry_run=0
pending=''
while [ $# -gt 0 ]; do
  case "$1" in
    --dry-run) dry_run=1 ;;
    --pending)
      shift
      pending="${1:-}"
      ;;
    *)
      echo "publish-pages: unknown argument '$1'" >&2
      exit 2
      ;;
  esac
  shift
done

fail() {
  echo "publish-pages: $1" >&2
  exit 1
}

# Publishing a build made from uncommitted work means the live site shows
# something no commit describes, and nobody can reproduce it later.
[ -z "$(git status --porcelain)" ] || fail 'working tree is dirty; commit or stash first'

branch=$(git rev-parse --abbrev-ref HEAD)
[ "$branch" = 'main' ] || fail "publishing from '$branch'; only main is publishable"

git fetch --quiet origin pages
head=$(git rev-parse HEAD)
if [ -n "$pending" ]; then
  [ "$head" = "$(git rev-parse "$pending")" ] ||
    fail "HEAD is $head but the pending push is $pending"
else
  git fetch --quiet origin main
  [ "$head" = "$(git rev-parse origin/main)" ] ||
    fail 'HEAD is not origin/main; push main first so the site matches a pushed commit'
fi

previous=$(git rev-parse origin/pages)

echo "publish-pages: building $head"
# `astro build` exits 0 even when vite fails to resolve an asset, so the exit
# code alone is not enough to know the build is whole. The gate greps for this
# too; repeat it here because this script is also run by hand.
build_log=$(mktemp)
trap 'rm -f "$build_log"' EXIT
pnpm run build >"$build_log" 2>&1 || {
  cat "$build_log" >&2
  fail 'astro build failed'
}
if grep -q '\[ERROR\]' "$build_log"; then
  cat "$build_log" >&2
  fail 'astro build logged an error and still exited 0; refusing to publish'
fi

# A build can succeed and still emit a stub -- that is exactly how this site
# would go down. Check the routes exist before overwriting anything live.
for route in index full-guide/index summary/index models/index agents/index \
  skills/index rules/index skills-review/index \
  hands-on/starter/index hands-on/rules/index; do
  [ -s "dist/$route.html" ] || fail "dist/$route.html missing or empty; refusing to publish"
done

# GIT_INDEX_FILE must name a path that does not exist yet: git reads an existing
# empty file as a truncated index and dies with "index file smaller than
# expected". mktemp -d gives a private directory to put that path in.
index_dir=$(mktemp -d)
index="$index_dir/index"
trap 'rm -rf "$index_dir"; rm -f "$build_log"' EXIT
# `--force` because the repository .gitignore lists `dist`; here `dist` *is* the
# work tree, so those rules would otherwise exclude everything we mean to ship.
GIT_INDEX_FILE="$index" git --work-tree=dist add --all --force .
tree=$(GIT_INDEX_FILE="$index" git write-tree)

if [ "$tree" = "$(git rev-parse "$previous^{tree}")" ]; then
  echo "publish-pages: dist is identical to the published tree; nothing to do"
  exit 0
fi

subject="chore: publish $(git rev-parse --short "$head")"
commit=$(git commit-tree "$tree" -p "$previous" -m "$subject

Built from main $head
$(git log -1 --format=%s "$head")")

if [ "$dry_run" -eq 1 ]; then
  echo "publish-pages: would push $commit to pages (previous $previous)"
  echo "publish-pages: dry run, nothing pushed"
  exit 0
fi

echo "publish-pages: rollback point is $previous"
echo "  git push --force origin $previous:refs/heads/pages"

# AF_PUBLISHING stops the pre-push hook recursing into this script.
AF_PUBLISHING=1 git push --force origin "$commit:refs/heads/pages"
echo "publish-pages: published $commit"
