Skip to content

Commit 51c608b

Browse files
chore: add git hooks and quickstart.sh
- Add hooks/post-commit: auto-bumps BUILD version after each commit (amend-based, no double-push) - Add hooks/pre-push: blocks direct main push, offers PyPI publish after push - Add quickstart.sh: one-command dev setup (installs hooks + pip install -e .)
1 parent 7626f26 commit 51c608b

3 files changed

Lines changed: 337 additions & 0 deletions

File tree

‎hooks/post-commit‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env bash
2+
# Post-commit hook for sageLLM repositories
3+
# Automatically bumps BUILD version (X.Y.Z.BUILD → X.Y.Z.BUILD+1) after each commit.
4+
# Version source of truth: src/<package>/_version.py
5+
#
6+
# This runs AFTER commit, amends it with the bumped version,
7+
# so the pre-push hook never needs to handle version bumping.
8+
9+
set -eo pipefail
10+
11+
# Recursion guard: skip if we're already inside an amend
12+
if [ -f ".git/SAGE_POST_COMMIT_RUNNING" ]; then
13+
exit 0
14+
fi
15+
16+
# Allow disabling via environment variable
17+
if [ "${SAGE_SKIP_VERSION_BUMP:-0}" = "1" ]; then
18+
exit 0
19+
fi
20+
21+
# Colors
22+
BLUE='\033[0;34m'
23+
GREEN='\033[0;32m'
24+
YELLOW='\033[1;33m'
25+
NC='\033[0m'
26+
27+
# Bump BUILD digit: X.Y.Z.N → X.Y.Z.(N+1), X.Y.Z → X.Y.Z.1
28+
bump_version() {
29+
local v="$1"
30+
if [[ "$v" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
31+
echo "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}.$((BASH_REMATCH[4] + 1))"
32+
return 0
33+
fi
34+
if [[ "$v" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
35+
echo "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}.1"
36+
return 0
37+
fi
38+
return 1
39+
}
40+
41+
# Find _version.py — single source of truth
42+
find_version_file() {
43+
find src -maxdepth 4 -name '_version.py' \
44+
-not -path '*/.git/*' \
45+
-not -path '*/dist/*' \
46+
-not -path '*/.egg-info/*' \
47+
-not -path '*/build/*' \
48+
2>/dev/null | head -1
49+
}
50+
51+
VERSION_FILE=$(find_version_file)
52+
if [ -z "$VERSION_FILE" ]; then
53+
exit 0 # Not a Python package repo, nothing to do
54+
fi
55+
56+
# Check if _version.py was already changed in this commit
57+
if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '_version.py'; then
58+
exit 0 # Developer manually bumped version, don't override
59+
fi
60+
61+
# Read current version from _version.py
62+
current_version=$(grep -oP '__version__ = "\K[^"]+' "$VERSION_FILE" 2>/dev/null || true)
63+
if [ -z "$current_version" ]; then
64+
exit 0
65+
fi
66+
67+
# Calculate new version
68+
if ! new_version=$(bump_version "$current_version"); then
69+
echo -e "${YELLOW}⚠️ Could not auto-bump version (invalid format: $current_version)${NC}"
70+
exit 0
71+
fi
72+
73+
echo -e "${BLUE}📦 Auto-bumping version: $current_version → $new_version${NC}"
74+
75+
# Lock to prevent recursion
76+
touch .git/SAGE_POST_COMMIT_RUNNING
77+
78+
# Update _version.py (single source of truth)
79+
sed -i "s/__version__ = \"${current_version}\"/__version__ = \"${new_version}\"/" "$VERSION_FILE"
80+
git add "$VERSION_FILE"
81+
82+
# Amend commit with version bump (--no-verify to skip re-triggering hooks)
83+
git commit --amend --no-edit --no-verify
84+
85+
# Cleanup
86+
rm -f .git/SAGE_POST_COMMIT_RUNNING
87+
88+
echo -e "${GREEN}✓ Version bumped to $new_version (commit amended)${NC}"
89+
exit 0

‎hooks/pre-push‎

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/bin/bash
2+
# Pre-push hook — block main push + optional PyPI publish
3+
#
4+
# Flow:
5+
# 1. Block direct push to main branch
6+
# 2. Display current version
7+
# 3. Check if version already exists on PyPI
8+
# 4. Offer to publish after push completes (background job)
9+
#
10+
# Version bumping is handled entirely by the post-commit hook.
11+
# This hook NEVER bumps version or creates additional commits.
12+
# → Single push only, no double-push.
13+
14+
# Recursion guard (unused now, kept for safety)
15+
if [ "${_SAGELLM_PP_RUNNING:-0}" = "1" ]; then
16+
exit 0
17+
fi
18+
19+
# Publish mode: "private" for most repos, "public" for sage-pypi-publisher
20+
PUBLISH_MODE=public
21+
22+
# Colors
23+
RED='\033[0;31m'
24+
YELLOW='\033[1;33m'
25+
GREEN='\033[0;32m'
26+
CYAN='\033[0;36m'
27+
BLUE='\033[0;34m'
28+
DIM='\033[2m'
29+
NC='\033[0m'
30+
31+
REPO_DIR="$(pwd)"
32+
REPO_NAME="$(basename "$REPO_DIR")"
33+
PUBLISH_LOG="/tmp/${REPO_NAME}-publish-$$.log"
34+
35+
# --- Block direct push to main ---
36+
block_main_push=false
37+
while read -r local_ref local_sha remote_ref remote_sha; do
38+
if [ "$local_ref" = "refs/heads/main" ] || [ "$remote_ref" = "refs/heads/main" ]; then
39+
block_main_push=true
40+
break
41+
fi
42+
done
43+
44+
if [ "$block_main_push" = true ]; then
45+
echo -e "${RED}✗ Direct push to main is forbidden${NC}"
46+
echo -e "${YELLOW} Please push to a feature branch and merge via PR.${NC}"
47+
exit 1
48+
fi
49+
50+
# Safe read: falls back to default if /dev/tty is unavailable (SSH, IDE, etc.)
51+
safe_read() {
52+
local varname="$1"
53+
local default="$2"
54+
if [ -t 0 ] || [ -c /dev/tty ] 2>/dev/null; then
55+
read -r "$varname" </dev/tty 2>/dev/null || eval "$varname=\"$default\""
56+
else
57+
eval "$varname=\"$default\""
58+
fi
59+
}
60+
61+
# Find _version.py — single source of truth
62+
find_version_file() {
63+
find . -maxdepth 4 -name '_version.py' \
64+
-not -path '*/node_modules/*' \
65+
-not -path '*/.git/*' \
66+
-not -path '*/dist/*' \
67+
-not -path '*/.egg-info/*' \
68+
-not -path '*/build/*' \
69+
2>/dev/null | head -1
70+
}
71+
72+
# Schedule PyPI publish as background job after push completes
73+
schedule_publish() {
74+
local version="$1"
75+
local package="$2"
76+
77+
if ! command -v sage-pypi-publisher &> /dev/null; then
78+
echo -e "${YELLOW}⚠ sage-pypi-publisher not found, skipping auto-publish${NC}"
79+
echo -e "${DIM} Install: pip install isage-pypi-publisher${NC}"
80+
return
81+
fi
82+
83+
echo -e "${GREEN}📦 PyPI publish scheduled (runs after push)${NC}"
84+
echo -e "${DIM} Log: tail -f ${PUBLISH_LOG}${NC}"
85+
86+
(
87+
# Wait for parent git push to finish
88+
GIT_PID="$PPID"
89+
while kill -0 "$GIT_PID" 2>/dev/null; do
90+
sleep 1
91+
done
92+
sleep 1
93+
94+
cd "$REPO_DIR" || exit 1
95+
96+
{
97+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
98+
echo "📦 Post-push: Building ${package} ${version}..."
99+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
100+
101+
rm -rf dist/ build/ *.egg-info 2>/dev/null || true
102+
103+
if sage-pypi-publisher build . --upload --no-dry-run --mode ${PUBLISH_MODE}; then
104+
echo ""
105+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
106+
echo "✓ Successfully uploaded ${package} ${version} to PyPI"
107+
echo "🔗 https://pypi.org/project/${package}/${version}/"
108+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
109+
else
110+
echo ""
111+
echo "✗ Failed to upload to PyPI (exit code: $?)"
112+
echo " Re-run manually: sage-pypi-publisher build . --upload --no-dry-run --mode ${PUBLISH_MODE}"
113+
fi
114+
} >> "$PUBLISH_LOG" 2>&1
115+
116+
# Print summary to terminal (user sees it after push output)
117+
if grep -q "Successfully uploaded" "$PUBLISH_LOG" 2>/dev/null; then
118+
echo -e "\n${GREEN}✓ PyPI: ${package} ${version} published${NC}"
119+
else
120+
echo -e "\n${RED}✗ PyPI publish failed. See: ${PUBLISH_LOG}${NC}"
121+
fi
122+
) &
123+
disown
124+
}
125+
126+
# --- Main Logic ---
127+
128+
if [ ! -f pyproject.toml ]; then
129+
exit 0
130+
fi
131+
132+
PACKAGE_NAME=$(grep -oP '^name = "\K[^"]+' pyproject.toml 2>/dev/null || echo "unknown")
133+
134+
# Get version from _version.py (single source of truth)
135+
VERSION_FILE=$(find_version_file)
136+
CURRENT_VERSION=""
137+
if [ -n "$VERSION_FILE" ]; then
138+
CURRENT_VERSION=$(grep -oP '__version__ = "\K[^"]+' "$VERSION_FILE" 2>/dev/null || true)
139+
fi
140+
141+
if [ -z "$CURRENT_VERSION" ]; then
142+
exit 0
143+
fi
144+
145+
echo -e "${GREEN}✓ [${REPO_NAME}] Version: ${CURRENT_VERSION}${NC}"
146+
147+
# Quick PyPI check (5s timeout, non-blocking)
148+
PYPI_CHECK_RESULT=1
149+
if [ "$PACKAGE_NAME" != "unknown" ] && command -v python3 &> /dev/null; then
150+
python3 - "$PACKAGE_NAME" "$CURRENT_VERSION" <<'PY' && PYPI_CHECK_RESULT=0 || PYPI_CHECK_RESULT=$?
151+
import json, sys, urllib.request
152+
try:
153+
with urllib.request.urlopen(f"https://pypi.org/pypi/{sys.argv[1]}/json", timeout=5) as r:
154+
sys.exit(0 if sys.argv[2] in json.load(r).get("releases", {}) else 1)
155+
except urllib.error.HTTPError as e:
156+
sys.exit(1 if e.code == 404 else 2)
157+
except (urllib.error.URLError, OSError, TimeoutError):
158+
sys.exit(2)
159+
except Exception:
160+
sys.exit(2)
161+
PY
162+
fi
163+
164+
if [ "$PYPI_CHECK_RESULT" -eq 0 ]; then
165+
echo -e "${YELLOW}⚠ ${PACKAGE_NAME} ${CURRENT_VERSION} already on PyPI — skipping publish${NC}"
166+
elif [ "$PYPI_CHECK_RESULT" -eq 2 ]; then
167+
echo -e "${DIM}(PyPI check skipped — network/timeout issue)${NC}"
168+
echo -e "${BLUE}📦 Publish ${CURRENT_VERSION} to PyPI after push? [Y/n]:${NC}"
169+
safe_read response "y"
170+
if [[ ! "$response" =~ ^[Nn]$ ]]; then
171+
schedule_publish "$CURRENT_VERSION" "$PACKAGE_NAME"
172+
fi
173+
else
174+
# Version not on PyPI — offer to publish
175+
echo -e "${BLUE}📦 Publish ${CURRENT_VERSION} to PyPI after push? [Y/n]:${NC}"
176+
safe_read response "y"
177+
if [[ ! "$response" =~ ^[Nn]$ ]]; then
178+
schedule_publish "$CURRENT_VERSION" "$PACKAGE_NAME"
179+
fi
180+
fi
181+
182+
# Exit 0 → push proceeds immediately, never blocked by build/upload
183+
exit 0

‎quickstart.sh‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/bin/bash
2+
# quickstart.sh - Setup script for sage-amms development
3+
4+
set -e
5+
6+
# Colors
7+
RED='\033[0;31m'
8+
GREEN='\033[0;32m'
9+
YELLOW='\033[1;33m'
10+
BLUE='\033[0;34m'
11+
CYAN='\033[0;36m'
12+
BOLD='\033[1m'
13+
NC='\033[0m'
14+
15+
# Get project root
16+
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17+
cd "$PROJECT_ROOT"
18+
19+
echo -e "${CYAN}${BOLD}╔════════════════════════════════════════════════════════════╗${NC}"
20+
echo -e "${CYAN}${BOLD}║ SAGE AMMs Development Setup ║${NC}"
21+
echo -e "${CYAN}${BOLD}╚════════════════════════════════════════════════════════════╝${NC}"
22+
echo ""
23+
24+
# Step 1: Install Git Hooks
25+
echo -e "${YELLOW}${BOLD}Step 1: Installing Git Hooks${NC}"
26+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
27+
28+
HOOKS_DIR="$PROJECT_ROOT/.git/hooks"
29+
TEMPLATE_DIR="$PROJECT_ROOT/hooks"
30+
31+
if [ ! -d "$HOOKS_DIR" ]; then
32+
echo -e "${RED}✗ Git repository not initialized${NC}"
33+
echo -e "${YELLOW}Run: git init${NC}"
34+
exit 1
35+
fi
36+
37+
if [ -d "$TEMPLATE_DIR" ]; then
38+
for hook in pre-commit pre-push post-commit; do
39+
if [ -f "$TEMPLATE_DIR/$hook" ]; then
40+
cp "$TEMPLATE_DIR/$hook" "$HOOKS_DIR/$hook"
41+
chmod +x "$HOOKS_DIR/$hook"
42+
echo -e "${GREEN}✓ Installed $hook hook${NC}"
43+
fi
44+
done
45+
else
46+
echo -e "${YELLOW}⚠ No hooks directory found, skipping...${NC}"
47+
fi
48+
49+
echo ""
50+
51+
# Step 2: Install in development mode
52+
echo -e "${YELLOW}${BOLD}Step 2: Installing isage-amms in development mode${NC}"
53+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
54+
55+
pip install -e .
56+
57+
echo ""
58+
echo -e "${GREEN}${BOLD}✓ Setup complete!${NC}"
59+
echo ""
60+
echo -e "${CYAN}Next steps:${NC}"
61+
echo -e " ${GREEN}1.${NC} Make changes to the code"
62+
echo -e " ${GREEN}2.${NC} Run tests: ${YELLOW}pytest tests/${NC}"
63+
echo -e " ${GREEN}3.${NC} Commit changes: ${YELLOW}git add . && git commit -m 'your message'${NC}"
64+
echo -e " ${GREEN}4.${NC} Push to GitHub: ${YELLOW}git push${NC} (pre-push hook will check version)"
65+
echo ""

0 commit comments

Comments
 (0)