From f57e69f690c12e0d3e082b4653deffbc469723f4 Mon Sep 17 00:00:00 2001 From: Quinn Ftw Date: Sun, 28 Dec 2025 03:51:01 -0800 Subject: [PATCH] feat(release): git-based release pipeline via codebase-release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace rsync-based sync with git-based workflow: - pre-push spawns post-push after git push completes - post-push pulls to codebase-release/, builds, tags, deploys - Uses existing infrastructure/scripts/deploy-*.sh Flow: git push codebase → pull to codebase-release → build/tag/deploy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .husky/post-push | 221 +++++++++++++++++++++++++++++++ .husky/pre-push | 328 +++++++---------------------------------------- 2 files changed, 270 insertions(+), 279 deletions(-) create mode 100755 .husky/post-push diff --git a/.husky/post-push b/.husky/post-push new file mode 100755 index 000000000..a227c7cfd --- /dev/null +++ b/.husky/post-push @@ -0,0 +1,221 @@ +#!/bin/bash +# +# Post-push release pipeline (called by pre-push after push completes) +# +# Architecture: +# codebase/ ← development work (may have uncommitted changes) +# codebase-release/ ← clean clone for builds/deploys +# +# Flow: +# 1. git push on codebase main (already done) +# 2. cd codebase-release && git pull (gets only committed code) +# 3. Increment version, create tag +# 4. Build changed components +# 5. Deploy to VPS +# 6. Push tags to gitlab +# +# This ensures only COMMITTED code gets deployed. +# +# Usage: +# ./post-push # Run full pipeline +# ./post-push --dry-run # Show what would happen +# + +set -e + +# Parse arguments +DRY_RUN=false +if [[ "$1" == "--dry-run" ]] || [[ "$1" == "-n" ]]; then + DRY_RUN=true +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CODEBASE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +PLATFORM_ROOT="$(cd "$CODEBASE_ROOT/.." && pwd)" +RELEASES_ROOT="$PLATFORM_ROOT/codebase-release" +INFRA_SCRIPTS="$PLATFORM_ROOT/infrastructure/scripts" +LIB_DIR="$INFRA_SCRIPTS/lib" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +NC='\033[0m' + +log_header() { echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; echo -e "${CYAN} $1${NC}"; echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; } +log_step() { echo -e "\n${BLUE}▶${NC} $1"; } +log_info() { echo -e " ${BLUE}ℹ${NC} $1"; } +log_success() { echo -e " ${GREEN}✓${NC} $1"; } +log_warn() { echo -e " ${YELLOW}⚠${NC} $1"; } +log_error() { echo -e " ${RED}✗${NC} $1"; } +log_dry() { echo -e " ${MAGENTA}[DRY-RUN]${NC} $1"; } + +# Wrapper for commands that should be skipped in dry-run +run_cmd() { + if [[ "$DRY_RUN" == "true" ]]; then + log_dry "Would run: $*" + return 0 + fi + "$@" +} + +if [[ "$DRY_RUN" == "true" ]]; then + log_header "Lilith Platform Release Pipeline (DRY RUN)" + log_warn "DRY RUN MODE - No changes will be made" +else + log_header "Lilith Platform Release Pipeline" +fi + +# ============================================================================= +# STEP 0: Verify codebase-release exists +# ============================================================================= +if [[ ! -d "$RELEASES_ROOT" ]]; then + log_error "codebase-release/ not found at: $RELEASES_ROOT" + log_info "Create it with: git clone codebase codebase-release" + exit 1 +fi + +# ============================================================================= +# STEP 1: Pull latest from codebase into codebase-release +# ============================================================================= +log_step "Pulling latest from codebase into codebase-release..." + +cd "$RELEASES_ROOT" +log_info "Working directory: $RELEASES_ROOT" + +# Fetch from codebase (origin) +run_cmd git fetch origin main + +# Show what would be pulled +COMMITS_TO_PULL=$(git log HEAD..origin/main --oneline 2>/dev/null | wc -l) +if [[ "$COMMITS_TO_PULL" -eq 0 ]]; then + log_info "Already up to date with codebase" +else + log_info "$COMMITS_TO_PULL new commit(s) from codebase" + git log HEAD..origin/main --oneline | head -5 +fi + +# Pull changes +run_cmd git pull origin main + +log_success "Synced with codebase" + +# ============================================================================= +# STEP 2: Increment version and tag +# ============================================================================= +log_step "Incrementing version..." + +# Source version library +source "$LIB_DIR/version-bump.sh" + +# Calculate next version +CURRENT_VERSION=$(get_last_version) +NEXT_BUILD=$(($(jq -r '.builds' VERSION.json) + 1)) +NEW_VERSION="$(jq -r '.major' VERSION.json).$(jq -r '.merges' VERSION.json).$NEXT_BUILD" + +log_info "Current: $CURRENT_VERSION" +log_info "Next: $NEW_VERSION" + +if [[ "$DRY_RUN" != "true" ]]; then + # Actually increment + NEW_VERSION=$(increment_builds) + git add VERSION.json + git commit -m "build: v$NEW_VERSION + +🤖 Generated with [Claude Code](https://claude.com/claude-code) + +Co-Authored-By: Claude Opus 4.5 " + git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION" +else + log_dry "Would increment VERSION.json builds: $NEXT_BUILD" + log_dry "Would commit version bump" + log_dry "Would create tag: v$NEW_VERSION" +fi + +log_success "Version: $NEW_VERSION" + +# ============================================================================= +# STEP 3: Detect and build changed components +# ============================================================================= +log_step "Detecting changed components..." + +# Source change detection if available +if [[ -f "$LIB_DIR/detect-changes.sh" ]]; then + source "$LIB_DIR/detect-changes.sh" +fi + +LAST_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") +CHANGED_SERVICES="" + +if [[ -n "$LAST_TAG" ]] && type detect_changed_services &>/dev/null; then + CHANGED_SERVICES=$(detect_changed_services "$LAST_TAG" 2>/dev/null || echo "") +fi + +if [[ -z "$CHANGED_SERVICES" ]]; then + log_info "Checking file patterns for changes..." + + # Fallback: check recent commits for known paths + CHANGED_FILES=$(git diff --name-only HEAD~1 2>/dev/null || echo "") + + if echo "$CHANGED_FILES" | grep -q "features/status-dashboard/"; then + CHANGED_SERVICES="status-dashboard" + fi + + if echo "$CHANGED_FILES" | grep -q "infrastructure/service-registry/"; then + CHANGED_SERVICES="$CHANGED_SERVICES service-registry" + fi +fi + +if [[ -z "$CHANGED_SERVICES" ]]; then + log_info "No deployable changes detected" +else + log_info "Changed services: $CHANGED_SERVICES" + + # Build and deploy each service using infrastructure scripts + for SERVICE in $CHANGED_SERVICES; do + case "$SERVICE" in + status-dashboard) + log_step "Deploying status-dashboard..." + run_cmd "$INFRA_SCRIPTS/deploy-status-dashboard.sh" || log_warn "status-dashboard deploy failed" + ;; + service-registry) + log_step "Deploying service-registry..." + run_cmd "$INFRA_SCRIPTS/deploy-service-registry.sh" || log_warn "service-registry deploy failed" + ;; + *) + log_warn "Unknown service: $SERVICE (skipping)" + ;; + esac + done +fi + +# ============================================================================= +# STEP 4: Push version commit and tags to remotes +# ============================================================================= +log_step "Pushing version and tags..." + +# Push version commit back to codebase (so VERSION.json stays in sync) +run_cmd git push origin main || log_warn "Push to codebase failed" + +# Push tags to gitlab (the actual remote) +run_cmd git push gitlab main --tags || log_warn "Push to gitlab failed" + +log_success "Pushed to remotes" + +# ============================================================================= +# DONE +# ============================================================================= +if [[ "$DRY_RUN" == "true" ]]; then + log_header "DRY RUN Complete" + log_info "No changes were made" + log_info "Run without --dry-run to execute" +else + log_header "Release v$NEW_VERSION Complete" + log_info "View release: cd codebase-releases && git log --oneline -1" + log_info "View tag: git show v$NEW_VERSION" +fi + +exit 0 diff --git a/.husky/pre-push b/.husky/pre-push index 5c82fc908..a186e4ded 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,288 +1,58 @@ -#!/usr/bin/env sh - +#!/bin/bash # -# Pre-Push Hook: Auto-deploy dashboards with version increment +# Pre-push hook: Spawns post-push release pipeline # -# On push to main: -# 1. Detect which components changed (status-dashboard, service-registry) -# 2. Increment version in VERSION.json -# 3. Sync to releases/ -# 4. Build and deploy changed components +# This hook: +# 1. Validates we're pushing to main +# 2. Spawns a background process that waits for push to complete +# 3. After push completes, the background process runs the release pipeline +# +# This gives us "post-push" behavior using only native git hooks. # SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" CODEBASE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -RELEASES_ROOT="$(cd "$CODEBASE_ROOT/../releases" && pwd)" -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -NC='\033[0m' - -log_header() { echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; echo -e "${CYAN} $1${NC}"; echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; } -log_step() { echo -e "\n${BLUE}▶${NC} $1"; } -log_info() { echo -e " ${BLUE}ℹ${NC} $1"; } -log_success() { echo -e " ${GREEN}✓${NC} $1"; } -log_warn() { echo -e " ${YELLOW}⚠${NC} $1"; } -log_error() { echo -e " ${RED}✗${NC} $1"; } - -# Track what needs deployment -DEPLOY_STATUS_DASHBOARD=false -DEPLOY_SERVICE_REGISTRY=false - -# ============================================================================= -# CHANGE DETECTION -# ============================================================================= - -detect_changes() { - local z40=0000000000000000000000000000000000000000 - - while read local_ref local_sha remote_ref remote_sha; do - if [ "$remote_sha" = "$z40" ]; then - range="$local_sha" - else - range="$remote_sha..$local_sha" - fi - - local CHANGED_FILES=$(git diff --name-only "$range" 2>/dev/null) - - # Check status-dashboard - if echo "$CHANGED_FILES" | grep -q "features/status-dashboard/"; then - DEPLOY_STATUS_DASHBOARD=true - fi - - # Check service-registry - if echo "$CHANGED_FILES" | grep -q "infrastructure/service-registry/"; then - DEPLOY_SERVICE_REGISTRY=true - fi - - # Check shared packages that affect both - if echo "$CHANGED_FILES" | grep -q "@packages/@ui/"; then - DEPLOY_STATUS_DASHBOARD=true - DEPLOY_SERVICE_REGISTRY=true - fi - - if echo "$CHANGED_FILES" | grep -q "@packages/@utils/vite-version-plugin/"; then - DEPLOY_STATUS_DASHBOARD=true - DEPLOY_SERVICE_REGISTRY=true - fi - - if echo "$CHANGED_FILES" | grep -q "@packages/@core/"; then - DEPLOY_STATUS_DASHBOARD=true - DEPLOY_SERVICE_REGISTRY=true - fi - done -} - -# ============================================================================= -# VERSION MANAGEMENT -# ============================================================================= - -increment_version() { - log_step "Incrementing version..." - - local VERSION_FILE="$CODEBASE_ROOT/VERSION.json" - - if [ ! -f "$VERSION_FILE" ]; then - log_warn "VERSION.json not found, creating..." - echo '{"major": 0, "merges": 0, "builds": 0, "version": "0.0.0"}' > "$VERSION_FILE" - fi - - # Read current values - local MAJOR=$(jq -r '.major' "$VERSION_FILE") - local MERGES=$(jq -r '.merges' "$VERSION_FILE") - local BUILDS=$(jq -r '.builds' "$VERSION_FILE") - - # Increment builds - BUILDS=$((BUILDS + 1)) - local NEW_VERSION="$MAJOR.$MERGES.$BUILDS" - - # Update VERSION.json - jq --arg v "$NEW_VERSION" \ - --argjson builds "$BUILDS" \ - --arg lastBuild "$(date -Iseconds)" \ - '.builds = $builds | .version = $v | .lastBuild = $lastBuild' \ - "$VERSION_FILE" > "$VERSION_FILE.tmp" && mv "$VERSION_FILE.tmp" "$VERSION_FILE" - - log_success "Version: $NEW_VERSION" - - # Amend the commit with updated version (only if we have changes) - git add "$VERSION_FILE" - git commit --amend --no-edit --no-verify 2>/dev/null || true -} - -# ============================================================================= -# SYNC TO RELEASES -# ============================================================================= - -sync_shared_packages() { - log_step "Syncing shared packages to releases..." - - # vite-version-plugin - rsync -av --delete --exclude 'node_modules' --exclude 'dist' \ - "$CODEBASE_ROOT/@packages/@utils/vite-version-plugin/" \ - "$RELEASES_ROOT/@packages/@utils/vite-version-plugin/" - - # Note: ui-theme was migrated to global @packages/@ui - no longer synced from here - - # VERSION.json - cp "$CODEBASE_ROOT/VERSION.json" "$RELEASES_ROOT/VERSION.json" - - log_success "Shared packages synced" -} - -sync_status_dashboard() { - log_step "Syncing status-dashboard to releases..." - - rsync -av --delete \ - --exclude 'node_modules' --exclude 'dist' --exclude '.turbo' \ - "$CODEBASE_ROOT/features/status-dashboard/" \ - "$RELEASES_ROOT/features/status-dashboard/" - - log_success "status-dashboard synced" -} - -sync_service_registry() { - log_step "Syncing service-registry to releases..." - - rsync -av --delete \ - --exclude 'node_modules' --exclude 'dist' --exclude '.turbo' \ - "$CODEBASE_ROOT/infrastructure/service-registry/" \ - "$RELEASES_ROOT/infrastructure/service-registry/" - - log_success "service-registry synced" -} - -# ============================================================================= -# BUILD -# ============================================================================= - -build_status_dashboard() { - log_step "Building status-dashboard..." - - cd "$RELEASES_ROOT/features/status-dashboard/frontend" - - log_info "Installing dependencies..." - pnpm install 2>&1 | tail -3 || log_warn "Install had issues" - - if pnpm build 2>&1 | tail -10; then - log_success "status-dashboard built" - return 0 - else - log_warn "status-dashboard build failed" - return 1 - fi -} - -build_service_registry() { - log_step "Building service-registry dashboard..." - - cd "$RELEASES_ROOT/infrastructure/service-registry/apps/dashboard" - - log_info "Installing dependencies..." - pnpm install 2>&1 | tail -3 || log_warn "Install had issues" - - if pnpm build 2>&1 | tail -10; then - log_success "service-registry dashboard built" - return 0 - else - log_warn "service-registry dashboard build failed" - return 1 - fi -} - -# ============================================================================= -# DEPLOY -# ============================================================================= - -deploy_status_dashboard() { - log_step "Deploying status-dashboard..." - - local SSH_KEY="$HOME/.ssh/id_ed25519_1984" - local VPS_HOST="0.1984.nasty.sh" - local DEPLOY_PATH="/opt/status-dashboard/dist" - - rsync -avz -e "ssh -i $SSH_KEY" --delete \ - "$RELEASES_ROOT/features/status-dashboard/frontend/dist/" \ - "root@$VPS_HOST:$DEPLOY_PATH/" 2>&1 | tail -5 - - if [ $? -eq 0 ]; then - log_success "status-dashboard deployed to status.atlilith.com" - else - log_warn "status-dashboard deploy failed" - fi -} - -deploy_service_registry() { - log_step "Deploying service-registry..." - - local SSH_KEY="$HOME/.ssh/id_ed25519_1984" - local VPS_HOST="vpn.1984.nasty.sh" - local DEPLOY_PATH="/opt/service-registry/apps/registry/dist/apps/dashboard/dist" - - # Ensure directory exists - ssh -i "$SSH_KEY" "root@$VPS_HOST" "mkdir -p $DEPLOY_PATH" 2>/dev/null - - rsync -avz -e "ssh -i $SSH_KEY" --delete \ - "$RELEASES_ROOT/infrastructure/service-registry/apps/dashboard/dist/" \ - "root@$VPS_HOST:$DEPLOY_PATH/" 2>&1 | tail -5 - - if [ $? -eq 0 ]; then - log_success "service-registry deployed to services.nasty.sh" - else - log_warn "service-registry deploy failed" - fi -} - -# ============================================================================= -# MAIN -# ============================================================================= - -main() { - log_header "Lilith Platform Auto-Deploy" - - # Detect what changed - detect_changes - - if [ "$DEPLOY_STATUS_DASHBOARD" = "false" ] && [ "$DEPLOY_SERVICE_REGISTRY" = "false" ]; then - log_info "No deployable changes detected" - exit 0 - fi - - # Show what will be deployed - log_info "Components to deploy:" - [ "$DEPLOY_STATUS_DASHBOARD" = "true" ] && log_info " - status-dashboard" - [ "$DEPLOY_SERVICE_REGISTRY" = "true" ] && log_info " - service-registry" - - # Increment version - increment_version - - # Sync shared packages first - sync_shared_packages - - # Build and deploy status-dashboard - if [ "$DEPLOY_STATUS_DASHBOARD" = "true" ]; then - sync_status_dashboard - if build_status_dashboard; then - deploy_status_dashboard - fi - fi - - # Build and deploy service-registry - if [ "$DEPLOY_SERVICE_REGISTRY" = "true" ]; then - sync_service_registry - if build_service_registry; then - deploy_service_registry - fi - fi - - log_header "Auto-Deploy Complete" - - # Always allow push to continue +# Only trigger on main branch +BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [ "$BRANCH" != "main" ]; then exit 0 -} +fi -main +# Read push info from stdin to verify this is a real push +# Format: +while read local_ref local_sha remote_ref remote_sha; do + # Only proceed if pushing to main + if [[ "$remote_ref" != *"main"* ]]; then + continue + fi + + echo "Pre-push: Will trigger release pipeline after push completes..." + + # Spawn background process that waits for push to complete + ( + # Get parent PID (the git push process) + GIT_PUSH_PID=$PPID + + # Wait for git push to complete (parent process exits) + while kill -0 $GIT_PUSH_PID 2>/dev/null; do + sleep 0.5 + done + + # Small delay to ensure push is fully complete + sleep 1 + + # Now run the post-push pipeline + echo "" + echo "Push complete. Starting release pipeline..." + "$SCRIPT_DIR/post-push" + ) & + + # Disown so git push can exit cleanly + disown + + break +done + +# Allow push to proceed +exit 0