init: document-haness 설계
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user