Files
document-haness/update.sh
T

203 lines
6.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Validate a fetched candidate, fast-forward the live checkout, then reinstall.
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CHECK_ONLY=0
INSTALL_ARGS=()
usage() {
cat <<'EOF'
Usage: ./update.sh [--check] [install.sh options]
--check Fetch and report only. Exit 0 when current/ahead, 10 when an
update is available, 1 when histories diverged, and 2 on an
invalid repository, missing upstream, or fetch failure.
The fetched commit is checked in a temporary detached worktree before the live
checkout is changed. Only a valid candidate is fast-forwarded, then install.sh
is reapplied. Remaining options are forwarded to install.sh, for example:
./update.sh --copy --force.
EOF
}
while (($#)); do
case "$1" in
--check) CHECK_ONLY=1 ;;
-h|--help) usage; exit 0 ;;
*) INSTALL_ARGS+=("$1") ;;
esac
shift
done
git_repo() {
git -C "$REPO_DIR" "$@"
}
if ! git_repo rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "error: update requires a Git working tree: $REPO_DIR" >&2
exit 2
fi
if [[ ! -f "$REPO_DIR/VERSION" ]]; then
echo "error: VERSION file is missing" >&2
exit 2
fi
OLD_VERSION="$(tr -d '[:space:]' < "$REPO_DIR/VERSION")"
if ! UPSTREAM="$(git_repo rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null)"; then
echo "error: current branch has no configured upstream; set one before updating" >&2
exit 2
fi
REMOTE_NAME="${UPSTREAM%%/*}"
echo "checking updates from $UPSTREAM ..."
if ! git_repo fetch --quiet "$REMOTE_NAME"; then
echo "error: git fetch failed for remote $REMOTE_NAME" >&2
exit 2
fi
LOCAL_REV="$(git_repo rev-parse HEAD)"
REMOTE_REV="$(git_repo rev-parse "$UPSTREAM")"
if ! MERGE_BASE="$(git_repo merge-base HEAD "$UPSTREAM" 2>/dev/null)"; then
echo "error: local and upstream have no merge base; automatic update stopped" >&2
exit 1
fi
if [[ "$LOCAL_REV" == "$REMOTE_REV" ]]; then
echo "already current: v$OLD_VERSION ($(git_repo rev-parse --short HEAD))"
exit 0
fi
if [[ "$MERGE_BASE" == "$REMOTE_REV" ]]; then
echo "local branch is ahead of $UPSTREAM; no update applied"
exit 0
fi
if [[ "$MERGE_BASE" != "$LOCAL_REV" ]]; then
echo "error: local and upstream histories diverged; automatic update stopped" >&2
echo "local=$(git_repo rev-parse --short HEAD) upstream=$(git_repo rev-parse --short "$UPSTREAM")" >&2
exit 1
fi
BEHIND="$(git_repo rev-list --count "HEAD..$UPSTREAM")"
echo "update available: $BEHIND commit(s)"
git_repo --no-pager log --oneline "HEAD..$UPSTREAM" -n 10
if ((CHECK_ONLY)); then
echo "check only; run ./update.sh to apply"
exit 10
fi
if [[ -n "$(git_repo status --porcelain --untracked-files=normal)" ]]; then
echo "error: working tree has local changes; commit, stash, or remove them before updating" >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "error: python3 is required for candidate contract checks" >&2
exit 2
fi
CANDIDATE_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/technical-doc-flow-update.XXXXXX")"
CANDIDATE_DIR="$CANDIDATE_ROOT/candidate"
cleanup_candidate() {
local status=$?
trap - EXIT
if [[ -n "${CANDIDATE_DIR:-}" ]]; then
git_repo worktree remove --force "$CANDIDATE_DIR" >/dev/null 2>&1 || true
fi
if [[ -n "${CANDIDATE_ROOT:-}" && -d "$CANDIDATE_ROOT" ]]; then
rm -rf -- "$CANDIDATE_ROOT"
fi
exit "$status"
}
trap cleanup_candidate EXIT
echo "preparing isolated candidate $REMOTE_REV ..."
if ! git_repo worktree add --quiet --detach "$CANDIDATE_DIR" "$REMOTE_REV"; then
echo "error: could not create the candidate validation worktree" >&2
exit 2
fi
for check_script in \
"$CANDIDATE_DIR/scripts/check_release_sync.py" \
"$CANDIDATE_DIR/scripts/build_quick_rules.py"; do
[[ -f "$check_script" ]] || {
echo "error: required candidate check is missing: $check_script; live checkout was not changed" >&2
exit 2
}
done
for shell_script in \
"$CANDIDATE_DIR/install.sh" \
"$CANDIDATE_DIR/uninstall.sh" \
"$CANDIDATE_DIR/update.sh"; do
[[ -f "$shell_script" ]] || {
echo "error: required candidate script is missing: $shell_script; live checkout was not changed" >&2
exit 2
}
bash -n "$shell_script" || {
echo "error: candidate shell validation failed: $shell_script; live checkout was not changed" >&2
exit 1
}
done
[[ -x "$CANDIDATE_DIR/install.sh" ]] || {
echo "error: candidate install.sh is not executable; live checkout was not changed" >&2
exit 2
}
echo "validating candidate release and path contracts ..."
python3 "$CANDIDATE_DIR/scripts/check_release_sync.py" || {
echo "error: candidate release contract validation failed; live checkout was not changed" >&2
exit 1
}
echo "validating candidate generated quick rules ..."
python3 "$CANDIDATE_DIR/scripts/build_quick_rules.py" --check || {
echo "error: candidate generated quick-rules validation failed; live checkout was not changed" >&2
exit 1
}
INSTALL_PREFLIGHT_ROOT="$CANDIDATE_ROOT/install-preflight"
echo "preflighting forwarded installer arguments against the candidate ..."
set +e
HOME="$INSTALL_PREFLIGHT_ROOT/home" \
TECH_DOC_FLOW_CLAUDE_HOME="$INSTALL_PREFLIGHT_ROOT/claude" \
TECH_DOC_FLOW_CODEX_HOME="$INSTALL_PREFLIGHT_ROOT/codex" \
TECH_DOC_FLOW_GEMINI_HOME="$INSTALL_PREFLIGHT_ROOT/gemini" \
"$CANDIDATE_DIR/install.sh" "${INSTALL_ARGS[@]}" --dry-run
INSTALL_PREFLIGHT_RC=$?
set -e
if [[ -e "$INSTALL_PREFLIGHT_ROOT" || -L "$INSTALL_PREFLIGHT_ROOT" ]]; then
echo "error: candidate install.sh --dry-run changed an isolated target; live checkout was not changed" >&2
exit 1
fi
if ((INSTALL_PREFLIGHT_RC != 0)); then
echo "error: candidate installer option preflight failed; live checkout was not changed" >&2
exit 2
fi
if [[ "$(git_repo rev-parse HEAD)" != "$LOCAL_REV" ]]; then
echo "error: live HEAD changed during candidate validation; update stopped" >&2
exit 1
fi
if [[ -n "$(git_repo status --porcelain --untracked-files=normal)" ]]; then
echo "error: working tree changed during candidate validation; update stopped" >&2
exit 1
fi
echo "candidate valid; applying pinned fast-forward update ..."
if ! git_repo merge --ff-only "$REMOTE_REV"; then
echo "error: fast-forward failed after candidate validation" >&2
exit 1
fi
[[ -x "$REPO_DIR/install.sh" ]] || {
echo "error: validated install.sh is missing or not executable after fast-forward" >&2
exit 2
}
"$REPO_DIR/install.sh" "${INSTALL_ARGS[@]}"
NEW_VERSION="$(tr -d '[:space:]' < "$REPO_DIR/VERSION")"
echo "update complete: v$OLD_VERSION -> v$NEW_VERSION"