mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-04-30 09:38:38 -05:00
2211 lines
99 KiB
YAML
2211 lines
99 KiB
YAML
name: '📚 Book · 🚀 Publish (Live)'
|
||
|
||
# Shared concurrency group prevents gh-pages conflicts with other live workflows
|
||
concurrency:
|
||
group: gh-pages-deploy
|
||
cancel-in-progress: false
|
||
|
||
# =============================================================================
|
||
# CONFIGURABLE VARIABLES - Edit these to customize the workflow
|
||
# =============================================================================
|
||
|
||
# AI Model Configuration
|
||
env:
|
||
DEFAULT_AI_MODEL: "gemma2:9b" # Default Ollama model (9b fits on GitHub runners)
|
||
FALLBACK_AI_MODEL: "llama3.2:3b" # Fallback if default model fails (smaller)
|
||
OLLAMA_TIMEOUT: "300" # Timeout for Ollama operations (seconds)
|
||
OLLAMA_RETRIES: "3" # Number of retries for Ollama calls
|
||
BUILD_TIMEOUT: "3600" # Timeout for build operations (1 hour)
|
||
|
||
# ==========================================================================
|
||
# PATH CONFIGURATION - Uses GitHub Repository Variables (Settings > Variables)
|
||
# ==========================================================================
|
||
# MLSysBook content lives under book/ to accommodate TinyTorch at root
|
||
# Use ${{ vars.BOOK_ROOT }}, ${{ vars.BOOK_QUARTO }}, etc. in workflow steps
|
||
# Variables: BOOK_ROOT, BOOK_DOCKER, BOOK_TOOLS, BOOK_QUARTO, BOOK_DEPS
|
||
|
||
# Quarto Configuration Files
|
||
QUARTO_HTML_CONFIG: "_quarto-html.yml" # HTML build configuration file
|
||
QUARTO_PDF_CONFIG: "_quarto-pdf.yml" # PDF build configuration file
|
||
QUARTO_MAIN_CONFIG: "_quarto.yml" # Main Quarto configuration file
|
||
|
||
# =============================================================================
|
||
# ARTIFACT COORDINATION SYSTEM
|
||
# =============================================================================
|
||
# The 📋 Quarto Build Container workflow creates build artifacts and an artifact manifest:
|
||
#
|
||
# artifact-manifest: JSON file declaring the names of HTML, PDF, and EPUB artifacts
|
||
# main-html-linux: Contains build/html/ (web version)
|
||
# main-pdf-linux: Contains build/pdf/Machine-Learning-Systems.pdf
|
||
# main-epub-linux: Contains build/epub/Machine-Learning-Systems.epub
|
||
# main-html-windows: Contains build/html/ (web version)
|
||
# main-pdf-windows: Contains build/pdf/Machine-Learning-Systems.pdf
|
||
# main-epub-windows: Contains build/epub/Machine-Learning-Systems.epub
|
||
#
|
||
# This workflow downloads the artifact manifest first to get the exact names,
|
||
# then downloads the HTML, PDF, and EPUB artifacts using those names for coordination.
|
||
# Quarto Build Container now uses dynamic matrix generation and explicit naming contracts.
|
||
#
|
||
# Artifact manifest structure:
|
||
# {
|
||
# "html_artifact_name": "main-html-linux",
|
||
# "pdf_artifact_name": "main-pdf-linux",
|
||
# "epub_artifact_name": "main-epub-linux",
|
||
# "build_timestamp": "20250115-143022",
|
||
# "commit_sha": "abc123...",
|
||
# "workflow_run_id": "12345",
|
||
# "detailed_manifest": "build-manifest-detailed",
|
||
# "parallel_builds": true,
|
||
# "extensible": true
|
||
# }
|
||
# =============================================================================
|
||
|
||
# Available AI Models (uncomment to use different models):
|
||
# - gemma2:9b (fast, good quality - recommended)
|
||
# - gemma2:27b (better quality, slower)
|
||
# - llama3.1:8b (good balance)
|
||
# - llama3.1:70b (best quality, slowest)
|
||
# - mistral:7b (fast, good for analysis)
|
||
# - codellama:7b (good for code-related changes)
|
||
|
||
# Manual trigger only - big red button!
|
||
# Only allow manual triggers from main and dev branches
|
||
#
|
||
# 🎯 PUBLISHING BEHAVIOR:
|
||
# ├── With dev_commit specified (e.g., "b5b452e"):
|
||
# │ ├── Merges EXACTLY that commit into main
|
||
# │ ├── Includes content + workflow files from that point in time
|
||
# │ └── Warning: You get the old workflow version too!
|
||
# │
|
||
# └── Without dev_commit (empty):
|
||
# ├── Merges latest dev branch into main
|
||
# ├── Includes newest content + newest workflow files
|
||
# └── Recommended for most releases
|
||
#
|
||
# 🌿 BRANCH CONTROL:
|
||
# Quarto Build Container workflow will automatically build from the main branch
|
||
# after the merge. The quarto build workflow can also be called manually with
|
||
# custom branch targets if needed for testing or special builds.
|
||
|
||
# Concurrency control: strict for production, flexible for testing
|
||
# Concurrency disabled - allow unlimited parallel builds
|
||
|
||
on:
|
||
workflow_dispatch:
|
||
inputs:
|
||
description:
|
||
description: 'What are you publishing? [Content updates and improvements]'
|
||
required: false
|
||
default: 'Content updates and improvements'
|
||
release_type:
|
||
description: 'Release type [patch]'
|
||
required: true
|
||
type: choice
|
||
options:
|
||
- 'patch'
|
||
- 'minor'
|
||
- 'major'
|
||
default: 'patch'
|
||
dev_commit:
|
||
description: 'Specific dev commit to publish (WARNING: includes old workflow files!) [latest dev]'
|
||
required: false
|
||
default: ''
|
||
confirm:
|
||
description: 'Type "PUBLISH" to confirm (safety check) [required]'
|
||
required: true
|
||
default: ''
|
||
ai_generated_notes:
|
||
description: 'Generate AI-enhanced release notes? [yes]'
|
||
required: true
|
||
type: choice
|
||
options:
|
||
- 'yes'
|
||
- 'no'
|
||
default: 'yes'
|
||
commit_status_timeout:
|
||
description: 'Number of status check attempts [180 = 3 hours at 60s intervals]'
|
||
required: false
|
||
default: '180'
|
||
commit_status_interval:
|
||
description: 'Seconds between status checks [60]'
|
||
required: false
|
||
default: '60'
|
||
previous_version:
|
||
description: 'Previous version to increment from (format: book-vX.Y.Z) [auto-detect from latest git tag]'
|
||
required: false
|
||
default: ''
|
||
testing_mode:
|
||
description: 'Enable testing mode (allows parallel runs, skips actual deployment) [no]'
|
||
required: false
|
||
type: choice
|
||
options:
|
||
- 'no'
|
||
- 'yes'
|
||
default: 'no'
|
||
deploy_target:
|
||
description: 'Deploy target (combined=book only, vol1, vol2, all=everything)'
|
||
required: false
|
||
type: choice
|
||
options:
|
||
- 'all'
|
||
- 'combined'
|
||
- 'vol1'
|
||
- 'vol2'
|
||
default: 'all'
|
||
|
||
permissions:
|
||
contents: write
|
||
actions: read
|
||
packages: read
|
||
|
||
jobs:
|
||
debug-log:
|
||
name: '📋 Debug & Audit Log'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
if: always()
|
||
|
||
steps:
|
||
- name: 📥 Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 📋 Log Workflow Inputs & Context
|
||
run: |
|
||
echo "## 📋 Workflow Debug & Audit Log" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Timestamp:** $(date -u)" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Workflow Run ID:** ${{ github.run_id }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Workflow Attempt:** ${{ github.run_attempt }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
|
||
echo "=== 📋 WORKFLOW DEBUG & AUDIT LOG ==="
|
||
echo "🕐 Workflow started at: $(date -u)"
|
||
echo "🔄 Run ID: ${{ github.run_id }}"
|
||
echo "🔄 Run Attempt: ${{ github.run_attempt }}"
|
||
echo "👤 Triggered by: ${{ github.actor }}"
|
||
echo "🌐 Repository: ${{ github.repository }}"
|
||
echo ""
|
||
|
||
echo "=== 📝 USER INPUTS ==="
|
||
echo "Description: '${{ github.event.inputs.description }}'"
|
||
echo "Release Type: '${{ github.event.inputs.release_type }}'"
|
||
echo "Dev Commit: '${{ github.event.inputs.dev_commit }}'"
|
||
echo "Confirmation: '${{ github.event.inputs.confirm }}'"
|
||
echo "AI Generated Notes: '${{ github.event.inputs.ai_generated_notes }}'"
|
||
echo "Status Check Timeout: '${{ github.event.inputs.commit_status_timeout }}' attempts"
|
||
echo "Status Check Interval: '${{ github.event.inputs.commit_status_interval }}' seconds"
|
||
echo ""
|
||
|
||
echo "### 📝 User Inputs:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Description:** ${{ github.event.inputs.description }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Release Type:** ${{ github.event.inputs.release_type }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Dev Commit:** ${{ github.event.inputs.dev_commit }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Confirmation:** ${{ github.event.inputs.confirm }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **AI Generated Notes:** ${{ github.event.inputs.ai_generated_notes }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Status Check Timeout:** ${{ github.event.inputs.commit_status_timeout }} attempts" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Status Check Interval:** ${{ github.event.inputs.commit_status_interval }} seconds" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
|
||
- name: 🔍 Log Git & Environment Context
|
||
run: |
|
||
echo "=== 🔍 GIT CONTEXT ==="
|
||
echo "Branch: ${{ github.ref_name }}"
|
||
echo "Ref: ${{ github.ref }}"
|
||
echo "SHA: ${{ github.sha }}"
|
||
echo "Event: ${{ github.event_name }}"
|
||
echo ""
|
||
|
||
echo "Git Status:"
|
||
git status --porcelain || echo "No git status available"
|
||
echo ""
|
||
|
||
echo "Recent Commits (last 5):"
|
||
git log --oneline -5 || echo "No git log available"
|
||
echo ""
|
||
|
||
echo "Remote branches:"
|
||
git branch -r | head -10 || echo "No remote branches info"
|
||
echo ""
|
||
|
||
echo "Latest tags:"
|
||
git tag --sort=-version:refname | head -10 || echo "No tags found"
|
||
echo ""
|
||
|
||
echo "### 🔍 Git Context:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **SHA:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Event:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
|
||
- name: 🛠️ Log Environment & AI Configuration
|
||
run: |
|
||
echo "=== 🛠️ ENVIRONMENT ==="
|
||
echo "Runner OS: ${{ runner.os }}"
|
||
echo "Default AI Model: ${{ env.DEFAULT_AI_MODEL }}"
|
||
echo "Fallback AI Model: ${{ env.FALLBACK_AI_MODEL }}"
|
||
echo "Ollama Timeout: ${{ env.OLLAMA_TIMEOUT }}"
|
||
echo "Ollama Retries: ${{ env.OLLAMA_RETRIES }}"
|
||
echo "Build Timeout: ${{ env.BUILD_TIMEOUT }}"
|
||
echo ""
|
||
|
||
echo "=== 🧪 VALIDATION CHECKS ==="
|
||
echo "Confirmation Valid: ${{ github.event.inputs.confirm == 'PUBLISH' }}"
|
||
echo "Branch Valid: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }}"
|
||
echo "Will Proceed: ${{ github.event.inputs.confirm == 'PUBLISH' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') }}"
|
||
echo ""
|
||
|
||
echo "### 🛠️ Environment:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Runner OS:** ${{ runner.os }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **AI Model:** ${{ env.DEFAULT_AI_MODEL }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Valid Confirmation:** ${{ github.event.inputs.confirm == 'PUBLISH' }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Valid Branch:** ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Will Proceed:** ${{ github.event.inputs.confirm == 'PUBLISH' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
|
||
- name: 📊 Log Previous Releases
|
||
run: |
|
||
echo "=== 📊 RELEASE HISTORY ==="
|
||
echo "Checking existing releases..."
|
||
|
||
# Get latest releases
|
||
RELEASES=$(curl -s \
|
||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||
-H "Accept: application/vnd.github.v3+json" \
|
||
"https://api.github.com/repos/${{ github.repository }}/releases?per_page=5" \
|
||
| jq -r '.[] | "\(.tag_name) - \(.published_at // "draft") - \(.draft)"' 2>/dev/null || echo "Unable to fetch releases")
|
||
|
||
echo "Recent releases:"
|
||
echo "$RELEASES"
|
||
echo ""
|
||
|
||
# Get latest tags
|
||
echo "Latest tags:"
|
||
git tag --sort=-version:refname | head -5 || echo "No tags found"
|
||
echo ""
|
||
|
||
echo "### 📊 Release History:" >> $GITHUB_STEP_SUMMARY
|
||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||
echo "$RELEASES" >> $GITHUB_STEP_SUMMARY
|
||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
|
||
validate-inputs:
|
||
name: '🔍 Validate Inputs'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
if: github.event.inputs.confirm == 'PUBLISH' && github.event.inputs.testing_mode != 'yes' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')
|
||
outputs:
|
||
new_version: ${{ steps.version.outputs.new_version }}
|
||
previous_version: ${{ steps.version.outputs.previous_version }}
|
||
release_type: ${{ steps.version.outputs.release_type }}
|
||
|
||
steps:
|
||
- name: 🔒 Check Branch Restriction
|
||
run: |
|
||
echo "🔒 Checking branch restrictions..."
|
||
echo "Current branch: ${{ github.ref_name }}"
|
||
echo "Current ref: ${{ github.ref }}"
|
||
|
||
if [[ "${{ github.ref }}" != "refs/heads/main" && "${{ github.ref }}" != "refs/heads/dev" ]]; then
|
||
echo "❌ ERROR: This workflow can only be triggered from 'main' or 'dev' branches"
|
||
echo "❌ Current branch: ${{ github.ref_name }}"
|
||
echo "❌ Please switch to 'main' or 'dev' branch before running this workflow"
|
||
exit 1
|
||
fi
|
||
|
||
echo "✅ Branch check passed - running from ${{ github.ref_name }}"
|
||
|
||
- name: 📥 Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 🔍 Validate dev commit
|
||
run: |
|
||
echo "🔍 Validating dev commit..."
|
||
|
||
# Get the commit to validate and trim whitespace
|
||
COMMIT_SHA=$(echo "${{ github.event.inputs.dev_commit }}" | xargs)
|
||
|
||
if [ -n "$COMMIT_SHA" ]; then
|
||
echo "📌 Using specified commit: $COMMIT_SHA"
|
||
|
||
# Verify commit exists and is from dev branch
|
||
if ! git cat-file -e "$COMMIT_SHA" 2>/dev/null; then
|
||
echo "❌ Commit $COMMIT_SHA does not exist!"
|
||
exit 1
|
||
fi
|
||
|
||
if ! git merge-base --is-ancestor "$COMMIT_SHA" origin/dev; then
|
||
echo "❌ Commit $COMMIT_SHA is not in dev branch!"
|
||
exit 1
|
||
fi
|
||
|
||
echo "✅ Commit $COMMIT_SHA is valid and from dev branch"
|
||
else
|
||
echo "📌 Using latest dev commit (no specific commit specified)"
|
||
fi
|
||
|
||
echo "✅ Ready to publish"
|
||
|
||
- name: 🏷️ Calculate Next Version
|
||
id: version
|
||
run: |
|
||
echo "🔄 Getting latest release version..."
|
||
|
||
# Use provided previous version or auto-detect
|
||
if [ -n "${{ github.event.inputs.previous_version }}" ]; then
|
||
LATEST_VERSION="${{ github.event.inputs.previous_version }}"
|
||
echo "📌 Using provided previous version: $LATEST_VERSION"
|
||
else
|
||
# Get latest git tag version, default to book-v0.0.0 if no tags exist
|
||
LATEST_VERSION=$(git tag -l "book-v*" | sort -V | tail -n1)
|
||
if [ -z "$LATEST_VERSION" ]; then
|
||
LATEST_VERSION="book-v0.0.0"
|
||
echo "📊 No git tags found, using default: $LATEST_VERSION"
|
||
else
|
||
echo "📊 Auto-detected latest git tag: $LATEST_VERSION"
|
||
fi
|
||
fi
|
||
|
||
# Remove 'book-v' prefix for calculation
|
||
VERSION_NUM=${LATEST_VERSION#book-v}
|
||
|
||
# Split version into components
|
||
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION_NUM"
|
||
|
||
# Handle empty or invalid versions
|
||
MAJOR=${MAJOR:-0}
|
||
MINOR=${MINOR:-0}
|
||
PATCH=${PATCH:-0}
|
||
|
||
echo "📊 Previous version components: $MAJOR.$MINOR.$PATCH"
|
||
|
||
# Calculate new version based on release type
|
||
case "${{ github.event.inputs.release_type }}" in
|
||
"major")
|
||
NEW_MAJOR=$((MAJOR + 1))
|
||
NEW_MINOR=0
|
||
NEW_PATCH=0
|
||
;;
|
||
"minor")
|
||
NEW_MAJOR=$MAJOR
|
||
NEW_MINOR=$((MINOR + 1))
|
||
NEW_PATCH=0
|
||
;;
|
||
"patch")
|
||
NEW_MAJOR=$MAJOR
|
||
NEW_MINOR=$MINOR
|
||
NEW_PATCH=$((PATCH + 1))
|
||
;;
|
||
esac
|
||
|
||
NEW_VERSION="book-v$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH"
|
||
echo "🎯 New version: $NEW_VERSION (${{ github.event.inputs.release_type }} release)"
|
||
echo "📋 Description: ${{ github.event.inputs.description }}"
|
||
|
||
# Export for other steps
|
||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||
echo "release_type=${{ github.event.inputs.release_type }}" >> $GITHUB_OUTPUT
|
||
echo "previous_version=$LATEST_VERSION" >> $GITHUB_OUTPUT
|
||
|
||
pre-flight-checks:
|
||
name: '🛫 Pre-Flight Validation'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 15
|
||
needs: validate-inputs
|
||
if: github.event.inputs.confirm == 'PUBLISH' && github.event.inputs.testing_mode != 'yes'
|
||
|
||
steps:
|
||
- name: 📥 Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 🔍 Validate Dev Branch Content
|
||
run: |
|
||
echo "🔍 Validating dev branch content before merge..."
|
||
|
||
# Switch to dev branch and pull latest
|
||
git checkout dev
|
||
git pull origin dev
|
||
|
||
# Check if dev commit exists and is valid
|
||
if [ -n "${{ github.event.inputs.dev_commit }}" ]; then
|
||
DEV_COMMIT=$(echo "${{ github.event.inputs.dev_commit }}" | xargs)
|
||
echo "📌 Validating specific commit: $DEV_COMMIT"
|
||
|
||
if ! git cat-file -e "$DEV_COMMIT" 2>/dev/null; then
|
||
echo "❌ Commit $DEV_COMMIT does not exist!"
|
||
exit 1
|
||
fi
|
||
|
||
if ! git merge-base --is-ancestor "$DEV_COMMIT" HEAD; then
|
||
echo "❌ Commit $DEV_COMMIT is not in current dev branch!"
|
||
exit 1
|
||
fi
|
||
|
||
# Checkout the specific commit for validation
|
||
git checkout "$DEV_COMMIT"
|
||
fi
|
||
|
||
echo "✅ Dev branch content validated"
|
||
|
||
- name: 📚 Validate Quarto Project Structure
|
||
run: |
|
||
echo "📚 Validating Quarto project structure..."
|
||
|
||
cd ${{ vars.BOOK_QUARTO }}
|
||
|
||
# Check critical files exist using environment variables
|
||
# Note: paths are relative to ${{ vars.BOOK_QUARTO }}/ after the cd above
|
||
REQUIRED_FILES=("${{ env.QUARTO_MAIN_CONFIG }}" "config/${{ env.QUARTO_HTML_CONFIG }}" "config/${{ env.QUARTO_PDF_CONFIG }}")
|
||
for file in "${REQUIRED_FILES[@]}"; do
|
||
if [ ! -f "$file" ]; then
|
||
echo "❌ Required file missing: $file"
|
||
exit 1
|
||
fi
|
||
echo "✅ Found: $file"
|
||
done
|
||
|
||
# Validate Quarto configuration
|
||
if command -v quarto >/dev/null 2>&1; then
|
||
echo "🔍 Checking Quarto configuration..."
|
||
if ! quarto check; then
|
||
echo "⚠️ Quarto check reported issues, but continuing..."
|
||
fi
|
||
else
|
||
echo "ℹ️ Quarto not available for validation in this environment"
|
||
fi
|
||
|
||
echo "✅ Quarto project structure validated"
|
||
|
||
- name: 🧪 Test Build Prerequisites
|
||
run: |
|
||
echo "🧪 Testing build prerequisites..."
|
||
|
||
# Check disk space (PDF builds need significant space)
|
||
echo "💾 Checking disk space..."
|
||
df -h
|
||
|
||
AVAILABLE_GB=$(df / | awk 'NR==2 {print int($4/1024/1024)}')
|
||
echo "📊 Available disk space: ${AVAILABLE_GB}GB"
|
||
|
||
if [ "$AVAILABLE_GB" -lt 5 ]; then
|
||
echo "❌ Insufficient disk space! Need at least 5GB, have ${AVAILABLE_GB}GB"
|
||
exit 1
|
||
fi
|
||
|
||
echo "✅ Sufficient disk space available"
|
||
|
||
# Test GitHub API access
|
||
echo "🔍 Testing GitHub API access..."
|
||
if ! curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||
"https://api.github.com/repos/${{ github.repository }}" >/dev/null; then
|
||
echo "❌ GitHub API access failed!"
|
||
exit 1
|
||
fi
|
||
echo "✅ GitHub API access confirmed"
|
||
|
||
echo "✅ All prerequisites validated"
|
||
|
||
- name: 🤖 Test AI System Availability
|
||
if: github.event.inputs.ai_generated_notes == 'yes'
|
||
run: |
|
||
echo "🤖 Testing if AI system can be installed for release notes..."
|
||
|
||
# Quick test: Can we download Ollama installer?
|
||
echo "🔍 Checking Ollama availability..."
|
||
if curl -fsSL --max-time 30 https://ollama.ai/install.sh > /dev/null; then
|
||
echo "✅ Ollama installer is accessible"
|
||
echo "🤖 AI-enhanced release notes will be available"
|
||
else
|
||
echo "⚠️ Ollama installer not accessible"
|
||
echo "📋 Will use git-log-only release notes instead"
|
||
echo "💡 This is not a failure - release notes will still be generated"
|
||
fi
|
||
|
||
echo "✅ AI system availability check complete"
|
||
|
||
- name: 📋 Pre-Flight Summary
|
||
run: |
|
||
echo "## 🛫 Pre-Flight Validation Complete" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Status:** All checks passed ✅" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Dev Commit:** ${{ github.event.inputs.dev_commit || 'latest' }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Target Version:** ${{ needs.validate-inputs.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Validation Time:** $(date)" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### ✅ Validated:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Dev branch content and commit validity" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Quarto project structure and configuration" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Build prerequisites (disk space, API access)" >> $GITHUB_STEP_SUMMARY
|
||
if [ "${{ github.event.inputs.ai_generated_notes }}" = "yes" ]; then
|
||
echo "- AI system (Ollama) installation and testing" >> $GITHUB_STEP_SUMMARY
|
||
fi
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "🚀 **Ready to proceed with merge and publication!**" >> $GITHUB_STEP_SUMMARY
|
||
|
||
update-version:
|
||
name: '📝 Update Version Number'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
needs: [validate-inputs, pre-flight-checks]
|
||
if: github.event.inputs.confirm == 'PUBLISH' && github.event.inputs.testing_mode != 'yes'
|
||
|
||
steps:
|
||
- name: 📥 Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 📝 Update Version in index.qmd
|
||
run: |
|
||
echo "📝 Updating version number in ${{ vars.BOOK_QUARTO }}/index.qmd..."
|
||
echo "🎯 This automatically updates the version displayed on the website"
|
||
echo "🔗 The version links to GitHub releases via assets/scripts/version-link.js"
|
||
echo ""
|
||
|
||
# Switch to dev branch first to update the file
|
||
git checkout dev
|
||
git pull origin dev
|
||
|
||
# Find and update the doi line with the new version
|
||
# The doi field is repurposed to show version (with custom label "Version")
|
||
# JavaScript makes it link to releases page instead of DOI registry
|
||
sed -i "s|doi: \".*\"|doi: \"${{ needs.validate-inputs.outputs.new_version }}\"|g" ${{ vars.BOOK_QUARTO }}/index.qmd
|
||
|
||
echo "✅ Version updated to ${{ needs.validate-inputs.outputs.new_version }}"
|
||
echo "📄 Updated line:"
|
||
cat ${{ vars.BOOK_QUARTO }}/index.qmd | grep "doi:" || echo "Could not verify doi field"
|
||
|
||
# Commit the version update
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||
|
||
git add ${{ vars.BOOK_QUARTO }}/index.qmd
|
||
git commit -m "chore: update version to ${{ needs.validate-inputs.outputs.new_version }}" || echo "No changes to commit"
|
||
git push origin dev
|
||
|
||
echo "✅ Version committed to dev branch"
|
||
echo "🔄 Next step: merge-to-main will include this version update"
|
||
|
||
merge-to-main:
|
||
name: '🔄 Merge to Main'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 15
|
||
needs: [validate-inputs, pre-flight-checks, update-version]
|
||
if: github.event.inputs.confirm == 'PUBLISH' && github.event.inputs.testing_mode != 'yes'
|
||
|
||
steps:
|
||
- name: 📥 Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 🔍 Check for workflow file changes
|
||
run: |
|
||
echo "🔍 Checking if workflow files will be modified in merge..."
|
||
|
||
# Check if workflow files are in the dev branch changes
|
||
if git diff --name-only origin/main..origin/dev | grep -q "\.github/workflows/"; then
|
||
echo "⚠️ Workflow files detected in dev branch!"
|
||
echo "📋 This will cause permission issues with publish-live workflow."
|
||
echo "💡 Please manually merge workflow changes first:"
|
||
echo " 1. Create PR for workflow changes"
|
||
echo " 2. Review and merge to main"
|
||
echo " 3. Then run publish-live for content only"
|
||
echo ""
|
||
echo "🔍 Workflow files in dev branch:"
|
||
git diff --name-only origin/main..origin/dev | grep "\.github/workflows/"
|
||
echo ""
|
||
echo "❌ Stopping to prevent permission issues"
|
||
exit 1
|
||
else
|
||
echo "✅ No workflow files detected - safe to proceed"
|
||
fi
|
||
|
||
- name: 🔄 Merge dev to main
|
||
run: |
|
||
echo "🔄 Configuring git..."
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||
|
||
echo "🔄 Fetching latest changes (including version update)..."
|
||
git fetch origin dev
|
||
git fetch origin main
|
||
|
||
echo "🔄 Switching to main branch..."
|
||
git checkout main
|
||
git pull origin main
|
||
|
||
# Safety check: Ensure main isn't ahead of specified dev commit
|
||
if [ -n "${{ github.event.inputs.dev_commit }}" ]; then
|
||
DEV_COMMIT=$(echo "${{ github.event.inputs.dev_commit }}" | xargs)
|
||
echo "🔍 Checking if main is ahead of specified commit $DEV_COMMIT..."
|
||
|
||
# Check if dev_commit is an ancestor of current main
|
||
if git merge-base --is-ancestor "$DEV_COMMIT" HEAD; then
|
||
echo "⚠️ MAIN IS AHEAD OF SPECIFIED COMMIT!"
|
||
echo "📊 Current main includes changes newer than $DEV_COMMIT"
|
||
echo ""
|
||
echo "🛑 This would create a mixed state (old dev + new main changes)"
|
||
echo "📋 Options to resolve:"
|
||
echo " A) Use latest dev instead (leave dev_commit empty)"
|
||
echo " B) Reset main to match dev commit (destructive):"
|
||
echo " git checkout main && git reset --hard $DEV_COMMIT && git push --force-with-lease"
|
||
echo " C) Merge dev branch normally first, then publish"
|
||
echo ""
|
||
echo "❌ Stopping to prevent untested mixed state"
|
||
exit 1
|
||
else
|
||
echo "✅ Safe to merge: $DEV_COMMIT is newer than current main"
|
||
fi
|
||
fi
|
||
|
||
echo "🔍 Checking for potential merge conflicts..."
|
||
# Test merge without committing
|
||
if ! git merge --no-commit --no-ff origin/dev 2>/dev/null; then
|
||
echo "❌ MERGE CONFLICTS DETECTED!"
|
||
echo "🛑 Automated merge cannot proceed due to conflicts."
|
||
echo "📋 Please resolve conflicts manually:"
|
||
echo " 1. git checkout main"
|
||
echo " 2. git pull origin main"
|
||
echo " 3. git merge dev"
|
||
echo " 4. Resolve conflicts and commit"
|
||
echo " 5. git push origin main"
|
||
git merge --abort
|
||
exit 1
|
||
fi
|
||
git reset --hard HEAD # Clean up test merge
|
||
|
||
echo "✅ No conflicts detected. Proceeding with merge..."
|
||
echo "🔄 Merging dev into main..."
|
||
|
||
# Debug: Show what dev_commit input was received
|
||
DEV_COMMIT_INPUT="${{ github.event.inputs.dev_commit }}"
|
||
echo "🔍 DEBUG: dev_commit input = '$DEV_COMMIT_INPUT'"
|
||
echo "🔍 DEBUG: Input length = ${#DEV_COMMIT_INPUT}"
|
||
|
||
# Determine which commit to merge
|
||
if [ -n "${{ github.event.inputs.dev_commit }}" ]; then
|
||
MERGE_COMMIT=$(echo "${{ github.event.inputs.dev_commit }}" | xargs)
|
||
echo "📌 SPECIFIC COMMIT MODE: Merging exact commit: $MERGE_COMMIT"
|
||
echo "⚠️ This includes content + workflow files from that point in time"
|
||
else
|
||
MERGE_COMMIT="origin/dev"
|
||
echo "📊 LATEST DEV MODE: Merging latest dev commit"
|
||
echo "✅ This includes newest content + newest workflow files"
|
||
fi
|
||
|
||
echo "🎯 Final merge target: $MERGE_COMMIT"
|
||
|
||
git merge "$MERGE_COMMIT" --no-ff -m "🚀 Release ${{ needs.validate-inputs.outputs.new_version }}: ${{ github.event.inputs.description }}
|
||
|
||
Merged dev branch to main for publication.
|
||
|
||
Release Type: ${{ github.event.inputs.release_type }}
|
||
Published by: ${{ github.actor }}
|
||
Dev Commit: ${MERGE_COMMIT}
|
||
Specific Commit: ${{ github.event.inputs.dev_commit || 'latest dev' }}
|
||
Description: ${{ github.event.inputs.description }}"
|
||
|
||
echo "✅ Merge completed successfully!"
|
||
|
||
- name: 🚀 Push merge to main
|
||
run: |
|
||
echo "🚀 Pushing merge to main branch..."
|
||
git push origin main
|
||
|
||
echo "✅ Main branch updated successfully!"
|
||
echo "📋 Next step: Monitor production build, then create release tag"
|
||
|
||
trigger-production-build:
|
||
name: '🚀 Trigger Production Build'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
needs: merge-to-main
|
||
if: github.event.inputs.confirm == 'PUBLISH' && github.event.inputs.testing_mode != 'yes'
|
||
outputs:
|
||
commit_sha: ${{ steps.commit.outputs.commit_sha }}
|
||
|
||
steps:
|
||
- name: 📥 Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 🚀 Get Current Commit SHA
|
||
id: commit
|
||
run: |
|
||
# Get the latest commit SHA from main branch
|
||
COMMIT_SHA=$(git rev-parse HEAD)
|
||
echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
|
||
echo "📌 Current commit SHA: $COMMIT_SHA"
|
||
|
||
# Matrix-driven production build (clean and simple!)
|
||
call-production-build:
|
||
name: '🚀 Call Production Build Matrix'
|
||
needs: [merge-to-main, trigger-production-build]
|
||
uses: ./.github/workflows/book-build-container.yml
|
||
with:
|
||
build_linux: true # Production builds Linux only for now
|
||
build_windows: false
|
||
build_html: true # HTML + PDF + EPUB for production
|
||
build_pdf: true
|
||
build_epub: true
|
||
build_target: ${{ github.event.inputs.deploy_target || 'all' }}
|
||
target: main
|
||
container_registry: 'ghcr.io'
|
||
container_tag: 'latest'
|
||
|
||
|
||
create-tag:
|
||
name: '🏷️ Create Release Tag (Final Step)'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
needs: [validate-inputs, download-and-deploy-artifacts]
|
||
if: github.event.inputs.confirm == 'PUBLISH' && github.event.inputs.testing_mode != 'yes'
|
||
|
||
steps:
|
||
- name: 📥 Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 🔄 Sync with latest main
|
||
run: |
|
||
echo "🔄 Configuring git..."
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||
|
||
echo "🔄 Switching to main and pulling latest changes..."
|
||
git checkout main
|
||
git pull origin main
|
||
|
||
echo "✅ Synced with latest main branch"
|
||
|
||
- name: 🏷️ Create Release Tag
|
||
run: |
|
||
echo "🏷️ Creating release tag ${{ needs.validate-inputs.outputs.new_version }}..."
|
||
echo "✅ Build AND deployment completed successfully - safe to create release tag"
|
||
|
||
# Check if tag already exists locally
|
||
if git tag -l "${{ needs.validate-inputs.outputs.new_version }}" | grep -q "${{ needs.validate-inputs.outputs.new_version }}"; then
|
||
echo "⚠️ Tag ${{ needs.validate-inputs.outputs.new_version }} already exists locally"
|
||
echo "🔄 Removing existing tag to recreate it..."
|
||
git tag -d ${{ needs.validate-inputs.outputs.new_version }}
|
||
fi
|
||
|
||
# Check if tag exists on remote
|
||
if git ls-remote --tags origin | grep -q "refs/tags/${{ needs.validate-inputs.outputs.new_version }}$"; then
|
||
echo "⚠️ Tag ${{ needs.validate-inputs.outputs.new_version }} already exists on remote"
|
||
echo "🔄 Removing remote tag to recreate it..."
|
||
git push origin --delete ${{ needs.validate-inputs.outputs.new_version }}
|
||
fi
|
||
|
||
# Create the tag on the latest main commit
|
||
git tag -a ${{ needs.validate-inputs.outputs.new_version }} -m "Release ${{ needs.validate-inputs.outputs.new_version }}: ${{ github.event.inputs.description }}"
|
||
echo "✅ Tag created successfully!"
|
||
|
||
- name: 🚀 Push tag for release tracking
|
||
run: |
|
||
echo "🚀 Pushing release tag for version tracking..."
|
||
git push origin ${{ needs.validate-inputs.outputs.new_version }}
|
||
|
||
echo "✅ Release tag pushed successfully!"
|
||
echo "🏷️ Tag: ${{ needs.validate-inputs.outputs.new_version }}"
|
||
echo "📋 Description: ${{ github.event.inputs.description }}"
|
||
echo "📊 This tag marks a successful build and tested release"
|
||
|
||
download-and-deploy-artifacts:
|
||
name: '📦 Download Artifacts & Deploy to GitHub Pages'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 15
|
||
needs: call-production-build
|
||
if: github.event.inputs.confirm == 'PUBLISH' && github.event.inputs.testing_mode != 'yes'
|
||
permissions:
|
||
contents: write # Allow write access to repository for gh-pages push
|
||
pages: write # Allow GitHub Pages deployment
|
||
actions: read # Allow reading of workflow artifacts
|
||
|
||
steps:
|
||
- name: 📥 Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 🔍 Validate API Contract
|
||
run: |
|
||
echo "🔍 Validating artifact names from API..."
|
||
|
||
HTML_ARTIFACT="${{ needs.call-production-build.outputs.linux_html_artifact }}"
|
||
PDF_ARTIFACT="${{ needs.call-production-build.outputs.linux_pdf_artifact }}"
|
||
EPUB_ARTIFACT="${{ needs.call-production-build.outputs.linux_epub_artifact }}"
|
||
|
||
echo "📄 HTML artifact name: '$HTML_ARTIFACT'"
|
||
echo "📑 PDF artifact name: '$PDF_ARTIFACT'"
|
||
echo "📚 EPUB artifact name: '$EPUB_ARTIFACT'"
|
||
|
||
# Fail explicitly if API didn't provide artifact names
|
||
if [ -z "$HTML_ARTIFACT" ]; then
|
||
echo "❌ CRITICAL: HTML artifact name not provided by build workflow API!"
|
||
echo "🔧 This indicates the workflow output contract is broken."
|
||
exit 1
|
||
fi
|
||
|
||
if [ -z "$PDF_ARTIFACT" ]; then
|
||
echo "❌ CRITICAL: PDF artifact name not provided by build workflow API!"
|
||
echo "🔧 This indicates the workflow output contract is broken."
|
||
exit 1
|
||
fi
|
||
|
||
if [ -z "$EPUB_ARTIFACT" ]; then
|
||
echo "❌ CRITICAL: EPUB artifact name not provided by build workflow API!"
|
||
echo "🔧 This indicates the workflow output contract is broken."
|
||
exit 1
|
||
fi
|
||
|
||
echo "✅ API contract validated - all artifact names provided"
|
||
|
||
- name: 📦 Download HTML Artifacts
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: ${{ needs.call-production-build.outputs.linux_html_artifact }}
|
||
path: ./html-temp
|
||
|
||
- name: 📦 Download PDF Artifacts
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: ${{ needs.call-production-build.outputs.linux_pdf_artifact }}
|
||
path: ./pdf-temp
|
||
|
||
- name: 📦 Download EPUB Artifacts
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: ${{ needs.call-production-build.outputs.linux_epub_artifact }}
|
||
path: ./epub-temp
|
||
continue-on-error: true
|
||
|
||
# =================================================================
|
||
# Volume I Artifacts (when deploy_target is vol1 or all)
|
||
# =================================================================
|
||
- name: 📦 Download Volume I HTML Artifacts
|
||
if: github.event.inputs.deploy_target == 'vol1' || github.event.inputs.deploy_target == 'all'
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: ${{ needs.call-production-build.outputs.linux_html_vol1_artifact }}
|
||
path: ./html-vol1-temp
|
||
continue-on-error: true
|
||
|
||
- name: 📦 Download Volume I PDF Artifacts
|
||
if: github.event.inputs.deploy_target == 'vol1' || github.event.inputs.deploy_target == 'all'
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: ${{ needs.call-production-build.outputs.linux_pdf_vol1_artifact }}
|
||
path: ./pdf-vol1-temp
|
||
continue-on-error: true
|
||
|
||
- name: 📦 Download Volume I EPUB Artifacts
|
||
if: github.event.inputs.deploy_target == 'vol1' || github.event.inputs.deploy_target == 'all'
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: ${{ needs.call-production-build.outputs.linux_epub_vol1_artifact }}
|
||
path: ./epub-vol1-temp
|
||
continue-on-error: true
|
||
|
||
# =================================================================
|
||
# Volume II Artifacts (when deploy_target is vol2 or all)
|
||
# =================================================================
|
||
- name: 📦 Download Volume II HTML Artifacts
|
||
if: github.event.inputs.deploy_target == 'vol2' || github.event.inputs.deploy_target == 'all'
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: ${{ needs.call-production-build.outputs.linux_html_vol2_artifact }}
|
||
path: ./html-vol2-temp
|
||
continue-on-error: true
|
||
|
||
- name: 📦 Download Volume II PDF Artifacts
|
||
if: github.event.inputs.deploy_target == 'vol2' || github.event.inputs.deploy_target == 'all'
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: ${{ needs.call-production-build.outputs.linux_pdf_vol2_artifact }}
|
||
path: ./pdf-vol2-temp
|
||
continue-on-error: true
|
||
|
||
- name: 📦 Download Volume II EPUB Artifacts
|
||
if: github.event.inputs.deploy_target == 'vol2' || github.event.inputs.deploy_target == 'all'
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: ${{ needs.call-production-build.outputs.linux_epub_vol2_artifact }}
|
||
path: ./epub-vol2-temp
|
||
continue-on-error: true
|
||
|
||
- name: 📋 Verify Downloaded Artifacts
|
||
run: |
|
||
echo "📦 Verifying downloaded artifacts..."
|
||
|
||
# Find HTML build directory - check if html-temp has content
|
||
if [ ! -d "html-temp" ] || [ -z "$(ls -A html-temp 2>/dev/null)" ]; then
|
||
echo "❌ HTML build directory not found or empty!"
|
||
echo "📊 HTML artifact contents:"
|
||
find html-temp -type f -o -type d | head -20
|
||
exit 1
|
||
fi
|
||
|
||
# Look for the actual HTML build structure
|
||
HTML_BUILD_DIR="html-temp"
|
||
if [ -d "html-temp/html" ]; then
|
||
HTML_BUILD_DIR="html-temp/html"
|
||
echo "✅ HTML build found in subdirectory: $HTML_BUILD_DIR"
|
||
else
|
||
echo "✅ HTML build found at root level: $HTML_BUILD_DIR"
|
||
fi
|
||
|
||
echo "📊 HTML structure preview:"
|
||
ls -la "$HTML_BUILD_DIR" | head -5
|
||
|
||
# Find PDF file
|
||
PDF_FILE=$(find pdf-temp -name "Machine-Learning-Systems.pdf" -type f | head -1)
|
||
if [ -z "$PDF_FILE" ] || [ ! -f "$PDF_FILE" ]; then
|
||
echo "❌ PDF file not found in extracted artifacts!"
|
||
echo "📊 PDF artifact contents:"
|
||
find pdf-temp -type f -o -type d | head -20
|
||
exit 1
|
||
fi
|
||
|
||
echo "✅ PDF found: $PDF_FILE"
|
||
echo "📊 PDF size: $(du -h "$PDF_FILE" | cut -f1)"
|
||
|
||
# Find EPUB file if available
|
||
EPUB_FILE=""
|
||
if [ -d "epub-temp" ]; then
|
||
EPUB_FILE=$(find epub-temp -name "Machine-Learning-Systems.epub" -type f | head -1)
|
||
if [ -z "$EPUB_FILE" ] || [ ! -f "$EPUB_FILE" ]; then
|
||
# Try alternative EPUB filenames
|
||
EPUB_FILE=$(find epub-temp -name "*.epub" -type f | head -1)
|
||
if [ -n "$EPUB_FILE" ] && [ -f "$EPUB_FILE" ]; then
|
||
echo "✅ EPUB found (alternative name): $EPUB_FILE"
|
||
echo "📊 EPUB size: $(du -h "$EPUB_FILE" | cut -f1)"
|
||
else
|
||
echo "⚠️ EPUB file not found in extracted artifacts - will skip EPUB deployment"
|
||
EPUB_FILE=""
|
||
fi
|
||
else
|
||
echo "✅ EPUB found: $EPUB_FILE"
|
||
echo "📊 EPUB size: $(du -h "$EPUB_FILE" | cut -f1)"
|
||
fi
|
||
else
|
||
echo "⚠️ No EPUB artifact extracted - will skip EPUB deployment"
|
||
fi
|
||
|
||
# Prepare combined site
|
||
echo "🔄 Preparing combined HTML site with PDF and EPUB..."
|
||
mkdir -p combined-site
|
||
|
||
# Copy HTML content
|
||
cp -r "$HTML_BUILD_DIR"/* combined-site/
|
||
|
||
# Create assets and downloads directories
|
||
mkdir -p combined-site/assets/downloads
|
||
|
||
# Copy PDF if available
|
||
if [ -n "$PDF_FILE" ] && [ -f "$PDF_FILE" ]; then
|
||
PDF_SIZE_MB=$(du -m "$PDF_FILE" | cut -f1)
|
||
echo "📊 PDF size: ${PDF_SIZE_MB}MB"
|
||
|
||
# Deploy PDF to both GitHub Pages and GitHub Release
|
||
cp "$PDF_FILE" combined-site/assets/downloads/Machine-Learning-Systems.pdf
|
||
echo "✅ PDF deployed to GitHub Pages (${PDF_SIZE_MB}MB)"
|
||
|
||
# Also copy PDF for GitHub release
|
||
cp "$PDF_FILE" "Machine-Learning-Systems.pdf"
|
||
echo "✅ PDF prepared for release"
|
||
else
|
||
echo "❌ PDF file not found - skipping PDF deployment"
|
||
fi
|
||
|
||
# Copy EPUB if available
|
||
if [ -n "$EPUB_FILE" ] && [ -f "$EPUB_FILE" ]; then
|
||
EPUB_SIZE_MB=$(du -m "$EPUB_FILE" | cut -f1)
|
||
echo "📊 EPUB size: ${EPUB_SIZE_MB}MB"
|
||
|
||
# Deploy EPUB to both GitHub Pages and GitHub Release
|
||
cp "$EPUB_FILE" combined-site/assets/downloads/Machine-Learning-Systems.epub
|
||
echo "✅ EPUB deployed to GitHub Pages (${EPUB_SIZE_MB}MB)"
|
||
|
||
# Also copy EPUB for GitHub release
|
||
cp "$EPUB_FILE" "Machine-Learning-Systems.epub"
|
||
echo "✅ EPUB prepared for release"
|
||
else
|
||
echo "❌ EPUB file not found - skipping EPUB deployment"
|
||
fi
|
||
|
||
echo "📊 Combined site structure:"
|
||
ls -la combined-site/ | head -10
|
||
echo "📊 Assets directory:"
|
||
ls -la combined-site/assets/
|
||
|
||
# Summary of what was prepared
|
||
echo "📋 Asset preparation complete:"
|
||
if [ -f "Machine-Learning-Systems.pdf" ]; then
|
||
echo " ✅ PDF: Ready for deployment"
|
||
else
|
||
echo " ❌ PDF: Not available"
|
||
fi
|
||
if [ -f "Machine-Learning-Systems.epub" ]; then
|
||
echo " ✅ EPUB: Ready for deployment"
|
||
else
|
||
echo " ❌ EPUB: Not available"
|
||
fi
|
||
|
||
# =================================================================
|
||
# Prepare Volume I site (if artifacts exist)
|
||
# =================================================================
|
||
if [ -d "html-vol1-temp" ]; then
|
||
echo "🔄 Preparing Volume I site..."
|
||
mkdir -p vol1-site
|
||
|
||
# Find HTML content
|
||
if [ -d "html-vol1-temp/html-vol1" ]; then
|
||
cp -r html-vol1-temp/html-vol1/* vol1-site/
|
||
else
|
||
cp -r html-vol1-temp/* vol1-site/
|
||
fi
|
||
|
||
mkdir -p vol1-site/assets/downloads
|
||
|
||
# Copy Vol1 PDF if available
|
||
VOL1_PDF=$(find pdf-vol1-temp -name "*.pdf" -type f 2>/dev/null | head -1)
|
||
if [ -n "$VOL1_PDF" ] && [ -f "$VOL1_PDF" ]; then
|
||
cp "$VOL1_PDF" vol1-site/assets/downloads/Machine-Learning-Systems-Vol1.pdf
|
||
echo " ✅ Vol1 PDF: Ready"
|
||
fi
|
||
|
||
# Copy Vol1 EPUB if available
|
||
VOL1_EPUB=$(find epub-vol1-temp -name "*.epub" -type f 2>/dev/null | head -1)
|
||
if [ -n "$VOL1_EPUB" ] && [ -f "$VOL1_EPUB" ]; then
|
||
cp "$VOL1_EPUB" vol1-site/assets/downloads/Machine-Learning-Systems-Vol1.epub
|
||
echo " ✅ Vol1 EPUB: Ready"
|
||
fi
|
||
|
||
echo "✅ Volume I site prepared"
|
||
fi
|
||
|
||
# =================================================================
|
||
# Prepare Volume II site (if artifacts exist)
|
||
# =================================================================
|
||
if [ -d "html-vol2-temp" ]; then
|
||
echo "🔄 Preparing Volume II site..."
|
||
mkdir -p vol2-site
|
||
|
||
# Find HTML content
|
||
if [ -d "html-vol2-temp/html-vol2" ]; then
|
||
cp -r html-vol2-temp/html-vol2/* vol2-site/
|
||
else
|
||
cp -r html-vol2-temp/* vol2-site/
|
||
fi
|
||
|
||
mkdir -p vol2-site/assets/downloads
|
||
|
||
# Copy Vol2 PDF if available
|
||
VOL2_PDF=$(find pdf-vol2-temp -name "*.pdf" -type f 2>/dev/null | head -1)
|
||
if [ -n "$VOL2_PDF" ] && [ -f "$VOL2_PDF" ]; then
|
||
cp "$VOL2_PDF" vol2-site/assets/downloads/Machine-Learning-Systems-Vol2.pdf
|
||
echo " ✅ Vol2 PDF: Ready"
|
||
fi
|
||
|
||
# Copy Vol2 EPUB if available
|
||
VOL2_EPUB=$(find epub-vol2-temp -name "*.epub" -type f 2>/dev/null | head -1)
|
||
if [ -n "$VOL2_EPUB" ] && [ -f "$VOL2_EPUB" ]; then
|
||
cp "$VOL2_EPUB" vol2-site/assets/downloads/Machine-Learning-Systems-Vol2.epub
|
||
echo " ✅ Vol2 EPUB: Ready"
|
||
fi
|
||
|
||
echo "✅ Volume II site prepared"
|
||
fi
|
||
|
||
# =================================================================
|
||
# Prepare Landing Page (copy from repo)
|
||
# =================================================================
|
||
if [ -d "${{ github.workspace }}/landing" ]; then
|
||
echo "🔄 Preparing landing page..."
|
||
mkdir -p landing-page
|
||
cp -r ${{ github.workspace }}/landing/* landing-page/
|
||
echo "✅ Landing page prepared"
|
||
fi
|
||
|
||
- name: 🚀 Deploy Combined Site to GitHub Pages (Production)
|
||
run: |
|
||
echo "🚀 Deploying to GitHub Pages..."
|
||
echo "🌐 Production URL: https://mlsysbook.ai/"
|
||
echo "📦 Deploy target: ${{ github.event.inputs.deploy_target }}"
|
||
|
||
DEPLOY_TARGET="${{ github.event.inputs.deploy_target }}"
|
||
|
||
# Clone gh-pages branch
|
||
git clone --depth=1 --branch=gh-pages https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git gh-pages-repo
|
||
|
||
cd gh-pages-repo
|
||
|
||
# Configure git identity inside the repository
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||
|
||
# =================================================================
|
||
# SUBDIRECTORY DEPLOYMENT STRUCTURE
|
||
# =================================================================
|
||
# Each subsite has its own directory:
|
||
# / - Landing page (AI Engineering)
|
||
# /book/ - Combined textbook (Vol I + II)
|
||
# /vol1/ - Volume I standalone
|
||
# /vol2/ - Volume II standalone
|
||
# /tinytorch/ - TinyTorch framework
|
||
# /kits/ - Hardware Kits
|
||
# /labs/ - Labs
|
||
# =================================================================
|
||
|
||
# Clean up stale root-level files from old deployment structure
|
||
echo "🧹 Removing stale root-level files..."
|
||
rm -rf contents/ assets/ site_libs/ tools/
|
||
rm -f _redirects netlify.toml search.json sitemap.xml site.webmanifest 404.html
|
||
|
||
# Remove development artifacts and symlinks that break GitHub Pages
|
||
echo "🧹 Removing development artifacts..."
|
||
rm -rf __pycache__/ .vscode/ .tito/ benchmark_results/ mlsysbook.egg-info/
|
||
rm -f .luarc.json
|
||
rm -rf .claude .github # Remove symlinks and dev-only directories
|
||
echo "✅ Stale files removed"
|
||
|
||
# =================================================================
|
||
# Deploy Combined Book to /book/ (if target is combined or all)
|
||
# =================================================================
|
||
if [[ "$DEPLOY_TARGET" == "combined" || "$DEPLOY_TARGET" == "all" ]]; then
|
||
echo "📦 Deploying combined textbook to /book/..."
|
||
rm -rf book/
|
||
mkdir -p book
|
||
cp -r ../combined-site/* book/
|
||
echo "✅ Combined textbook deployed to /book/"
|
||
fi
|
||
|
||
# =================================================================
|
||
# Deploy Volume I to /vol1/ (if target is vol1 or all)
|
||
# =================================================================
|
||
if [[ "$DEPLOY_TARGET" == "vol1" || "$DEPLOY_TARGET" == "all" ]]; then
|
||
if [ -d "../vol1-site" ]; then
|
||
echo "📦 Deploying Volume I to /vol1/..."
|
||
rm -rf vol1/
|
||
mkdir -p vol1
|
||
cp -r ../vol1-site/* vol1/
|
||
echo "✅ Volume I deployed to /vol1/"
|
||
else
|
||
echo "⚠️ Volume I site not found - skipping"
|
||
fi
|
||
fi
|
||
|
||
# =================================================================
|
||
# Deploy Volume II to /vol2/ (if target is vol2 or all)
|
||
# =================================================================
|
||
if [[ "$DEPLOY_TARGET" == "vol2" || "$DEPLOY_TARGET" == "all" ]]; then
|
||
if [ -d "../vol2-site" ]; then
|
||
echo "📦 Deploying Volume II to /vol2/..."
|
||
rm -rf vol2/
|
||
mkdir -p vol2
|
||
cp -r ../vol2-site/* vol2/
|
||
echo "✅ Volume II deployed to /vol2/"
|
||
else
|
||
echo "⚠️ Volume II site not found - skipping"
|
||
fi
|
||
fi
|
||
|
||
# =================================================================
|
||
# Deploy Landing Page to root (if target is all)
|
||
# =================================================================
|
||
if [[ "$DEPLOY_TARGET" == "all" ]]; then
|
||
if [ -f "../landing-page/index.html" ]; then
|
||
echo "📦 Deploying landing page to root..."
|
||
cp ../landing-page/index.html .
|
||
cp ../landing-page/logo.png . 2>/dev/null || true
|
||
echo "✅ Landing page deployed to root"
|
||
else
|
||
echo "⚠️ Landing page not found - creating redirect to /book/"
|
||
echo '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="refresh" content="0;url=/book/"><link rel="canonical" href="https://mlsysbook.ai/book/"><title>Redirecting...</title></head><body><p>Redirecting to <a href="/book/">ML Systems</a>...</p></body></html>' > index.html
|
||
fi
|
||
else
|
||
# For non-all deployments, keep existing root or create redirect
|
||
if [ ! -f "index.html" ]; then
|
||
echo "📦 Creating root redirect to /book/..."
|
||
echo '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="refresh" content="0;url=/book/"><link rel="canonical" href="https://mlsysbook.ai/book/"><title>Redirecting...</title></head><body><p>Redirecting to <a href="/book/">ML Systems</a>...</p></body></html>' > index.html
|
||
fi
|
||
fi
|
||
|
||
# Ensure CNAME file exists at root for custom domain
|
||
if [ ! -f "CNAME" ]; then
|
||
echo "mlsysbook.ai" > CNAME
|
||
echo "✅ CNAME file created for mlsysbook.ai"
|
||
else
|
||
echo "✅ CNAME file already exists"
|
||
fi
|
||
|
||
# Ensure .nojekyll exists at root
|
||
touch .nojekyll
|
||
|
||
echo "📊 Deployed content structure:"
|
||
ls -la | head -15
|
||
if [ -d "book" ]; then
|
||
echo "📊 Book directory contents:"
|
||
ls -la book/ | head -10
|
||
fi
|
||
if [ -d "vol1" ]; then
|
||
echo "📊 Vol1 directory contents:"
|
||
ls -la vol1/ | head -10
|
||
fi
|
||
if [ -d "vol2" ]; then
|
||
echo "📊 Vol2 directory contents:"
|
||
ls -la vol2/ | head -10
|
||
fi
|
||
|
||
# Add all files to git
|
||
git add .
|
||
|
||
# Check if there are actually changes to commit
|
||
echo "🔍 Checking for changes to deploy..."
|
||
if git diff --cached --quiet; then
|
||
echo "❌ CRITICAL: No changes detected in staging area!"
|
||
echo "🔧 This indicates the site content wasn't properly copied or is identical to existing content."
|
||
echo "📊 Current directory contents:"
|
||
ls -la | head -10
|
||
exit 1
|
||
else
|
||
echo "✅ Changes detected - proceeding with deployment"
|
||
CHANGED_FILES=$(git diff --cached --name-only | wc -l)
|
||
echo "📊 Files to be deployed: $CHANGED_FILES"
|
||
fi
|
||
|
||
# Commit with comprehensive validation
|
||
echo "📝 Creating deployment commit..."
|
||
COMMIT_MSG="🚀 Deploy release ${{ needs.validate-inputs.outputs.new_version }} to /book/ from commit ${{ github.sha }}
|
||
|
||
Combined HTML site with PDF and EPUB assets for download.
|
||
|
||
- HTML: Interactive web textbook at /book/
|
||
- PDF: /book/assets/downloads/Machine-Learning-Systems.pdf
|
||
- EPUB: /book/assets/downloads/Machine-Learning-Systems.epub (if available)
|
||
- Release: ${{ needs.validate-inputs.outputs.new_version }}
|
||
- Files updated: $CHANGED_FILES"
|
||
|
||
if git commit -m "$COMMIT_MSG"; then
|
||
echo "✅ Commit created successfully"
|
||
COMMIT_HASH=$(git rev-parse HEAD)
|
||
echo "📋 Commit hash: $COMMIT_HASH"
|
||
else
|
||
echo "❌ CRITICAL: Failed to create commit!"
|
||
echo "🔧 Git status:"
|
||
git status
|
||
exit 1
|
||
fi
|
||
|
||
# Push with validation
|
||
echo "🚀 Pushing to gh-pages branch..."
|
||
if git push origin gh-pages; then
|
||
echo "✅ Push successful!"
|
||
|
||
# Verify the push actually updated the remote
|
||
echo "🔍 Verifying remote update..."
|
||
REMOTE_HASH=$(git ls-remote origin gh-pages | cut -f1)
|
||
if [ "$COMMIT_HASH" = "$REMOTE_HASH" ]; then
|
||
echo "✅ Remote branch updated successfully"
|
||
echo "📋 Remote commit hash matches local: $REMOTE_HASH"
|
||
else
|
||
echo "⚠️ WARNING: Remote hash doesn't match local commit"
|
||
echo "📋 Local: $COMMIT_HASH"
|
||
echo "📋 Remote: $REMOTE_HASH"
|
||
fi
|
||
else
|
||
echo "❌ CRITICAL: Failed to push to gh-pages!"
|
||
echo "🔧 This could be due to permissions or network issues."
|
||
exit 1
|
||
fi
|
||
|
||
echo ""
|
||
echo "🎉 GitHub Pages deployment completed successfully!"
|
||
echo "📊 Deployment Summary:"
|
||
echo " - Files updated: $CHANGED_FILES"
|
||
echo " - Commit hash: $COMMIT_HASH"
|
||
echo " - Branch: gh-pages"
|
||
echo " - Deploy path: /book/"
|
||
echo ""
|
||
echo "🌐 Site URLs:"
|
||
echo " - Textbook: https://mlsysbook.ai/book/"
|
||
echo " - TinyTorch: https://mlsysbook.ai/tinytorch/"
|
||
echo " - Hardware Kits: https://mlsysbook.ai/kits/"
|
||
echo " - Root (redirects to /book/): https://mlsysbook.ai/"
|
||
echo ""
|
||
echo "📄 Direct Asset Links:"
|
||
echo " - PDF: https://mlsysbook.ai/book/assets/downloads/Machine-Learning-Systems.pdf"
|
||
echo " - EPUB: https://mlsysbook.ai/book/assets/downloads/Machine-Learning-Systems.epub"
|
||
echo ""
|
||
echo "⏰ Note: Changes may take 1-5 minutes to appear due to CDN caching."
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 📤 Upload PDF Artifact for GitHub Release
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: pdf-artifact
|
||
path: Machine-Learning-Systems.pdf
|
||
|
||
- name: 📤 Upload EPUB Artifact for GitHub Release
|
||
if: success()
|
||
run: |
|
||
if [ -f "Machine-Learning-Systems.epub" ]; then
|
||
echo "✅ EPUB file found for upload"
|
||
else
|
||
echo "⚠️ No EPUB file found - creating empty artifact to prevent workflow failure"
|
||
touch Machine-Learning-Systems.epub
|
||
fi
|
||
|
||
- name: 📤 Upload EPUB Artifact
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: epub-artifact
|
||
path: Machine-Learning-Systems.epub
|
||
if-no-files-found: warn
|
||
|
||
generate-release-notes:
|
||
name: '📝 Generate Release Notes'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 30
|
||
needs: [validate-inputs, download-and-deploy-artifacts]
|
||
if: github.event.inputs.confirm == 'PUBLISH' && github.event.inputs.testing_mode != 'yes'
|
||
|
||
steps:
|
||
- name: 📥 Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 📄 Download PDF from previous job
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: pdf-artifact
|
||
path: ./
|
||
|
||
- name: 📝 Generate Release Notes
|
||
run: |
|
||
echo "📝 Generating release notes..."
|
||
echo "🤖 AI Enhancement Mode: ${{ github.event.inputs.ai_generated_notes }}"
|
||
if [ "${{ github.event.inputs.ai_generated_notes }}" = "yes" ]; then
|
||
echo " ✅ Will use AI to enhance git log (if AI system is available)"
|
||
else
|
||
echo " 📋 Will use git log only (clean, reliable format)"
|
||
fi
|
||
echo ""
|
||
|
||
# Generate git log between versions
|
||
PREVIOUS_VERSION="${{ needs.validate-inputs.outputs.previous_version }}"
|
||
CURRENT_VERSION="${{ needs.validate-inputs.outputs.new_version }}"
|
||
|
||
echo "📊 Generating git log from $PREVIOUS_VERSION to current commit..."
|
||
|
||
# Create detailed git log
|
||
echo "# Release Notes for $CURRENT_VERSION" > git_changes.md
|
||
echo "" >> git_changes.md
|
||
echo "## Changes since $PREVIOUS_VERSION" >> git_changes.md
|
||
echo "" >> git_changes.md
|
||
|
||
# Get commit log with details
|
||
if git rev-parse "$PREVIOUS_VERSION" >/dev/null 2>&1; then
|
||
echo "✅ Previous version tag $PREVIOUS_VERSION found"
|
||
|
||
# Summary format for AI processing
|
||
git log --oneline ${PREVIOUS_VERSION}..HEAD > git_log_summary.txt
|
||
|
||
# Detailed format for inclusion in release
|
||
echo "### Commit Summary:" >> git_changes.md
|
||
git log --oneline ${PREVIOUS_VERSION}..HEAD >> git_changes.md
|
||
echo "" >> git_changes.md
|
||
|
||
echo "### Detailed Changes:" >> git_changes.md
|
||
git log --pretty=format:"- **%s** (%h) by %an%n %b" ${PREVIOUS_VERSION}..HEAD >> git_changes.md
|
||
|
||
echo "📊 Found $(git rev-list --count ${PREVIOUS_VERSION}..HEAD) commits since $PREVIOUS_VERSION"
|
||
else
|
||
echo "⚠️ Previous version tag $PREVIOUS_VERSION not found, using all commits"
|
||
git log --oneline -20 > git_log_summary.txt
|
||
echo "### Recent Commits:" >> git_changes.md
|
||
git log --oneline -20 >> git_changes.md
|
||
fi
|
||
|
||
echo "✅ Git log generated and saved to files"
|
||
|
||
if [ "${{ github.event.inputs.ai_generated_notes }}" = "yes" ]; then
|
||
echo ""
|
||
echo "🤖 AI Enhancement Mode Selected - installing Ollama for release notes..."
|
||
|
||
# Install Ollama for this job (each job runs in isolation)
|
||
echo "🤖 Installing Ollama..."
|
||
if curl -fsSL https://ollama.ai/install.sh | sh; then
|
||
echo "✅ Ollama installed successfully"
|
||
|
||
# Start Ollama service
|
||
echo "🚀 Starting Ollama service..."
|
||
ollama serve &
|
||
|
||
# Wait for service to be ready
|
||
echo "⏳ Waiting for Ollama service..."
|
||
OLLAMA_READY=false
|
||
for i in {1..30}; do
|
||
if ollama list >/dev/null 2>&1; then
|
||
echo "✅ Ollama service ready after ${i}0 seconds"
|
||
OLLAMA_READY=true
|
||
break
|
||
fi
|
||
echo "⏳ Waiting... (${i}/30)"
|
||
sleep 10
|
||
done
|
||
|
||
if [ "$OLLAMA_READY" = "true" ]; then
|
||
# Pull the model
|
||
MODEL="${{ env.DEFAULT_AI_MODEL }}"
|
||
echo "📦 Pulling AI model: $MODEL"
|
||
if timeout ${{ env.OLLAMA_TIMEOUT }} ollama pull $MODEL; then
|
||
echo "✅ Model $MODEL ready for release notes generation"
|
||
AI_GENERATION_FAILED=false
|
||
else
|
||
echo "⚠️ Model pull failed, using git log only"
|
||
AI_GENERATION_FAILED=true
|
||
fi
|
||
else
|
||
echo "⚠️ Ollama service failed to start, using git log only"
|
||
AI_GENERATION_FAILED=true
|
||
fi
|
||
else
|
||
echo "⚠️ Ollama installation failed, using git log only"
|
||
AI_GENERATION_FAILED=true
|
||
fi
|
||
|
||
# Generate AI-enhanced release notes using git log
|
||
if [ "$AI_GENERATION_FAILED" != "true" ]; then
|
||
echo "📝 Generating AI-enhanced release notes from git log..."
|
||
|
||
# Create AI prompt with git log content
|
||
{
|
||
echo "Generate release notes for MLSysBook ${{ needs.validate-inputs.outputs.new_version }} - an open-source Machine Learning Systems textbook."
|
||
echo ""
|
||
echo "AUDIENCE: Students, researchers, practitioners, contributors, and educators using the textbook"
|
||
echo "PURPOSE: Professional announcement of improvements and enhancements in this release"
|
||
echo ""
|
||
echo "RELEASE DESCRIPTION: ${{ github.event.inputs.description }}"
|
||
echo "RELEASE TYPE: ${{ github.event.inputs.release_type }}"
|
||
echo ""
|
||
echo "COMMITS:"
|
||
cat git_log_summary.txt
|
||
echo ""
|
||
echo "STYLE REQUIREMENTS:"
|
||
echo "- Academic, professional tone following established MLSysBook release pattern"
|
||
echo "- Focus on USER VALUE: what readers, educators, and students gain"
|
||
echo "- Educational impact: how this improves learning outcomes"
|
||
echo "- Clear structure with appropriate emojis for readability"
|
||
echo "- NO development metrics (commits, technical debt, internal processes)"
|
||
echo "- Write like a scholarly publication announcement, not marketing material"
|
||
echo ""
|
||
echo "LANGUAGE GUIDELINES:"
|
||
echo "EXCELLENT examples of user-focused language:"
|
||
echo "✅ 'Enhanced visualizations with improved clarity and understanding'"
|
||
echo "✅ 'Streamlined mathematical notation for better accessibility'"
|
||
echo "✅ 'Updated code examples reflecting latest ML frameworks'"
|
||
echo "✅ 'Improved accessibility features for diverse learners'"
|
||
echo "✅ 'Faster build process for reliable access'"
|
||
echo ""
|
||
echo "AVOID these generic/internal phrases:"
|
||
echo "❌ 'Various improvements across chapters'"
|
||
echo "❌ 'Enhanced development workflow'"
|
||
echo "❌ 'Updated content structure'"
|
||
echo "❌ 'Fixed issues and bugs'"
|
||
echo "❌ 'Improved codebase quality'"
|
||
echo "❌ ANY mention of specific chapter names or numbers"
|
||
echo ""
|
||
echo "REQUIRED STRUCTURE:"
|
||
echo "# Release ${{ needs.validate-inputs.outputs.new_version }}: [Professional Title Based on Main Theme]"
|
||
echo "IMPORTANT: Use the EXACT version number ${{ needs.validate-inputs.outputs.new_version }} in the title"
|
||
echo ""
|
||
echo "[Professional intro paragraph explaining this release's educational focus and value]"
|
||
echo ""
|
||
echo "## ✨ Major Features"
|
||
echo ""
|
||
echo "### 📖 Content Improvements"
|
||
echo "* [3-5 specific content enhancements that improve learning experience]"
|
||
echo "* Focus on: visualizations, explanations, examples, mathematical clarity"
|
||
echo ""
|
||
echo "### 🛠️ Technical Excellence"
|
||
echo "* [3-5 infrastructure improvements that users actually notice]"
|
||
echo "* Focus on: accessibility, performance, reliability, user experience"
|
||
echo ""
|
||
echo "### 🎓 Educational Innovation"
|
||
echo "* [2-3 improvements specifically for educators and learners]"
|
||
echo "* Focus on: teaching features, learning aids, practical applications"
|
||
echo ""
|
||
echo "## 🌟 Key Achievements"
|
||
echo "[Highlight the most impactful improvements for different user groups]"
|
||
echo ""
|
||
echo "## 🔬 Educational Impact"
|
||
echo "[Explain how these changes improve learning outcomes and educational value]"
|
||
echo ""
|
||
echo "## 🌐 Access Your Enhanced Textbook"
|
||
echo "- 📖 **Online Version**: [mlsysbook.ai](https://mlsysbook.ai)"
|
||
echo "- 📄 **PDF Download**: Available from release assets"
|
||
echo "- 📚 **EPUB Version**: Available from release assets"
|
||
echo "- 🧪 **Labs & Exercises**: Hands-on learning materials"
|
||
echo ""
|
||
echo "## 📞 Community & Contributions"
|
||
echo "This release incorporates feedback from educators, students, and practitioners. We welcome continued engagement through our [GitHub repository](https://github.com/harvard-edge/cs249r_book)."
|
||
echo ""
|
||
echo "---"
|
||
echo "*Development Period*: [Timeframe based on release type]"
|
||
echo "*Repository*: [harvard-edge/cs249r_book](https://github.com/harvard-edge/cs249r_book)"
|
||
echo "*Focus*: [Main theme of this release]"
|
||
} > ai_prompt.txt
|
||
|
||
# Generate AI release notes
|
||
if ollama run $MODEL < ai_prompt.txt > ai_release_notes.md 2>/dev/null; then
|
||
echo "✅ AI release notes generated successfully"
|
||
|
||
# Use AI-generated notes directly (they include proper header)
|
||
{
|
||
cat ai_release_notes.md
|
||
echo ""
|
||
echo "---"
|
||
echo ""
|
||
echo "## Full Change Log"
|
||
echo ""
|
||
cat git_changes.md
|
||
} > "release_notes_${{ needs.validate-inputs.outputs.new_version }}.md"
|
||
else
|
||
echo "⚠️ AI generation failed, using git log only"
|
||
AI_GENERATION_FAILED=true
|
||
fi
|
||
fi
|
||
|
||
# If AI generation failed, fall back to git log only
|
||
if [ "$AI_GENERATION_FAILED" = "true" ]; then
|
||
echo "📋 Falling back to git log only release notes"
|
||
{
|
||
echo "# Release ${{ needs.validate-inputs.outputs.new_version }}: ${{ github.event.inputs.description }}"
|
||
echo ""
|
||
echo "## Overview"
|
||
echo "This ${{ github.event.inputs.release_type }} release includes the following changes:"
|
||
echo ""
|
||
cat git_changes.md
|
||
} > "release_notes_${{ needs.validate-inputs.outputs.new_version }}.md"
|
||
fi
|
||
else
|
||
echo ""
|
||
echo "📋 Git Log Only Mode - creating clean release notes from git history"
|
||
|
||
# Create release notes from git log only
|
||
{
|
||
echo "# Release ${{ needs.validate-inputs.outputs.new_version }}: ${{ github.event.inputs.description }}"
|
||
echo ""
|
||
echo "## Overview"
|
||
echo "This ${{ github.event.inputs.release_type }} release includes the following changes:"
|
||
echo ""
|
||
cat git_changes.md
|
||
} > "release_notes_${{ needs.validate-inputs.outputs.new_version }}.md"
|
||
fi
|
||
|
||
# Show final release notes
|
||
if [ -f "release_notes_${{ needs.validate-inputs.outputs.new_version }}.md" ]; then
|
||
echo "✅ Release notes generated successfully"
|
||
echo "📄 Release notes file details:"
|
||
ls -la "release_notes_${{ needs.validate-inputs.outputs.new_version }}.md"
|
||
echo "📝 Release notes content (first 50 lines):"
|
||
head -50 "release_notes_${{ needs.validate-inputs.outputs.new_version }}.md"
|
||
else
|
||
echo "❌ Failed to generate release notes - creating basic fallback"
|
||
# Create a basic fallback release notes file
|
||
{
|
||
echo "# Release ${{ needs.validate-inputs.outputs.new_version }}: ${{ github.event.inputs.description }}"
|
||
echo ""
|
||
echo "## Release Information"
|
||
echo "- **Version**: ${{ needs.validate-inputs.outputs.new_version }}"
|
||
echo "- **Type**: ${{ github.event.inputs.release_type }} release"
|
||
echo "- **Description**: ${{ github.event.inputs.description }}"
|
||
echo ""
|
||
echo "## Changes"
|
||
echo "Please see the git commit history for detailed changes."
|
||
} > "release_notes_${{ needs.validate-inputs.outputs.new_version }}.md"
|
||
echo "✅ Basic release notes created as fallback"
|
||
fi
|
||
|
||
- name: 📤 Upload Release Notes Artifact
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: release-notes
|
||
path: release_notes_${{ needs.validate-inputs.outputs.new_version }}.md
|
||
|
||
- name: 📤 Upload Git Log Artifacts
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: git-changes
|
||
path: |
|
||
git_changes.md
|
||
git_log_summary.txt
|
||
ai_release_notes.md
|
||
|
||
create-release:
|
||
name: '📦 Create GitHub Release'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
needs: [validate-inputs, generate-release-notes, create-tag]
|
||
if: github.event.inputs.confirm == 'PUBLISH' && github.event.inputs.testing_mode != 'yes'
|
||
|
||
steps:
|
||
- name: 📥 Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 📄 Download PDF from previous job
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: pdf-artifact
|
||
path: ./
|
||
|
||
- name: 📚 Download EPUB from previous job
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: epub-artifact
|
||
path: ./
|
||
|
||
- name: 📝 Download release notes
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: release-notes
|
||
path: ./
|
||
|
||
- name: 📦 Create GitHub Release with PDF
|
||
run: |
|
||
echo "📦 Creating GitHub Release ${{ needs.validate-inputs.outputs.new_version }}..."
|
||
echo "📋 Release details:"
|
||
echo " - Tag: ${{ needs.validate-inputs.outputs.new_version }}"
|
||
echo " - Name: ${{ needs.validate-inputs.outputs.new_version }}: ${{ github.event.inputs.description }}"
|
||
echo " - Repository: ${{ github.repository }}"
|
||
|
||
# Use the AI-generated release notes
|
||
RELEASE_NOTES_FILE="release_notes_${{ needs.validate-inputs.outputs.new_version }}.md"
|
||
|
||
if [ -f "$RELEASE_NOTES_FILE" ]; then
|
||
echo "📄 Using AI-generated release notes:"
|
||
cat "$RELEASE_NOTES_FILE"
|
||
else
|
||
echo "❌ Release notes file not found!"
|
||
exit 1
|
||
fi
|
||
|
||
# Create the release as a DRAFT for manual editing
|
||
echo "🚀 Creating GitHub release as DRAFT..."
|
||
|
||
# Properly escape the release notes for JSON
|
||
ESCAPED_BODY=$(cat "$RELEASE_NOTES_FILE" | jq -Rs .)
|
||
|
||
# Create JSON payload using jq to ensure proper escaping
|
||
JSON_PAYLOAD=$(jq -n \
|
||
--arg tag "${{ needs.validate-inputs.outputs.new_version }}" \
|
||
--arg name "${{ needs.validate-inputs.outputs.new_version }}: ${{ github.event.inputs.description }}" \
|
||
--argjson body "$ESCAPED_BODY" \
|
||
'{
|
||
tag_name: $tag,
|
||
name: $name,
|
||
body: $body,
|
||
draft: true,
|
||
prerelease: false
|
||
}')
|
||
|
||
RELEASE_RESPONSE=$(curl -s \
|
||
-X POST \
|
||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||
-H "Accept: application/vnd.github.v3+json" \
|
||
-H "Content-Type: application/json" \
|
||
"https://api.github.com/repos/${{ github.repository }}/releases" \
|
||
-d "$JSON_PAYLOAD")
|
||
|
||
echo "📊 API Response:"
|
||
echo "$RELEASE_RESPONSE" | jq '.'
|
||
|
||
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
|
||
RELEASE_URL=$(echo "$RELEASE_RESPONSE" | jq -r '.html_url')
|
||
|
||
if [ "$RELEASE_ID" != "null" ] && [ -n "$RELEASE_ID" ]; then
|
||
echo "✅ Draft release created successfully!"
|
||
echo "📊 Release ID: $RELEASE_ID"
|
||
echo "🔗 Release URL: $RELEASE_URL"
|
||
echo "📝 Next step: Edit release notes manually and publish"
|
||
echo "release_id=$RELEASE_ID" >> $GITHUB_ENV
|
||
echo "release_url=$RELEASE_URL" >> $GITHUB_ENV
|
||
else
|
||
echo "❌ Failed to create release!"
|
||
echo "📊 Error details:"
|
||
echo "$RELEASE_RESPONSE" | jq -r '.message // "Unknown error"'
|
||
echo "$RELEASE_RESPONSE" | jq -r '.errors[]?.message // empty'
|
||
exit 1
|
||
fi
|
||
|
||
- name: 📄 Upload PDF to Release Assets
|
||
run: |
|
||
echo "📄 Uploading PDF to release assets..."
|
||
|
||
if [ ! -f "Machine-Learning-Systems.pdf" ]; then
|
||
echo "❌ PDF file not found!"
|
||
exit 1
|
||
fi
|
||
|
||
echo "📊 PDF size: $(du -h Machine-Learning-Systems.pdf | cut -f1)"
|
||
|
||
# Upload the PDF to the release
|
||
UPLOAD_RESPONSE=$(curl -s \
|
||
-X POST \
|
||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||
-H "Accept: application/vnd.github.v3+json" \
|
||
-H "Content-Type: application/pdf" \
|
||
--data-binary @Machine-Learning-Systems.pdf \
|
||
"https://uploads.github.com/repos/${{ github.repository }}/releases/${{ env.release_id }}/assets?name=Machine-Learning-Systems.pdf")
|
||
|
||
echo "📊 Upload Response:"
|
||
echo "$UPLOAD_RESPONSE" | jq '.'
|
||
|
||
UPLOAD_ID=$(echo "$UPLOAD_RESPONSE" | jq -r '.id')
|
||
|
||
if [ "$UPLOAD_ID" != "null" ] && [ -n "$UPLOAD_ID" ]; then
|
||
echo "✅ PDF uploaded successfully to release!"
|
||
echo "📊 Asset ID: $UPLOAD_ID"
|
||
echo "🔗 Download URL: https://github.com/${{ github.repository }}/releases/download/${{ needs.validate-inputs.outputs.new_version }}/Machine-Learning-Systems.pdf"
|
||
else
|
||
echo "❌ Failed to upload PDF to release!"
|
||
echo "📊 Error details:"
|
||
echo "$UPLOAD_RESPONSE" | jq -r '.message // "Unknown error"'
|
||
exit 1
|
||
fi
|
||
|
||
- name: 📚 Upload EPUB to Release Assets
|
||
run: |
|
||
echo "📚 Checking for EPUB file to upload..."
|
||
|
||
if [ ! -f "Machine-Learning-Systems.epub" ]; then
|
||
echo "⚠️ EPUB file not found - skipping EPUB upload"
|
||
exit 0
|
||
fi
|
||
|
||
# Check if it's an empty file (created as placeholder)
|
||
if [ ! -s "Machine-Learning-Systems.epub" ]; then
|
||
echo "⚠️ EPUB file is empty (placeholder) - skipping EPUB upload"
|
||
exit 0
|
||
fi
|
||
|
||
echo "📚 Uploading EPUB to release assets..."
|
||
echo "📊 EPUB size: $(du -h Machine-Learning-Systems.epub | cut -f1)"
|
||
|
||
# Upload the EPUB to the release
|
||
UPLOAD_RESPONSE=$(curl -s \
|
||
-X POST \
|
||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||
-H "Accept: application/vnd.github.v3+json" \
|
||
-H "Content-Type: application/epub+zip" \
|
||
--data-binary @Machine-Learning-Systems.epub \
|
||
"https://uploads.github.com/repos/${{ github.repository }}/releases/${{ env.release_id }}/assets?name=Machine-Learning-Systems.epub")
|
||
|
||
echo "📊 Upload Response:"
|
||
echo "$UPLOAD_RESPONSE" | jq '.'
|
||
|
||
UPLOAD_ID=$(echo "$UPLOAD_RESPONSE" | jq -r '.id')
|
||
|
||
if [ "$UPLOAD_ID" != "null" ] && [ -n "$UPLOAD_ID" ]; then
|
||
echo "✅ EPUB uploaded successfully to release!"
|
||
echo "📊 Asset ID: $UPLOAD_ID"
|
||
echo "🔗 Download URL: https://github.com/${{ github.repository }}/releases/download/${{ needs.validate-inputs.outputs.new_version }}/Machine-Learning-Systems.epub"
|
||
else
|
||
echo "⚠️ Failed to upload EPUB to release (continuing anyway)!"
|
||
echo "📊 Error details:"
|
||
echo "$UPLOAD_RESPONSE" | jq -r '.message // "Unknown error"'
|
||
fi
|
||
|
||
summary:
|
||
name: '📋 Publication Summary'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
needs: [validate-inputs, create-release]
|
||
if: github.event.inputs.confirm == 'PUBLISH' && github.event.inputs.testing_mode != 'yes'
|
||
|
||
steps:
|
||
- name: 📋 Publication Summary
|
||
run: |
|
||
echo "## 📚 Textbook Publication Complete! 🎉" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Version Released:** ${{ needs.validate-inputs.outputs.new_version }} (${{ needs.validate-inputs.outputs.release_type }})" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Previous Version:** ${{ needs.validate-inputs.outputs.previous_version }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Content Published:** ${{ github.event.inputs.description }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Published by:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Source Commit:** ${{ github.event.inputs.dev_commit || 'latest' }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Publication Time:** $(date)" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Release ID:** ${{ env.release_id || 'N/A' }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Release URL:** ${{ env.release_url || 'N/A' }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🔍 Debug Information:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Workflow Run ID:** ${{ github.run_id }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Run Attempt:** ${{ github.run_attempt }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Commit SHA:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **AI Generated Notes:** ${{ github.event.inputs.ai_generated_notes }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 📝 Changes:" >> $GITHUB_STEP_SUMMARY
|
||
echo "${{ github.event.inputs.description }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🔄 What happened:" >> $GITHUB_STEP_SUMMARY
|
||
echo "1. ✅ Verified dev branch tests passed" >> $GITHUB_STEP_SUMMARY
|
||
echo "2. ✅ Calculated new version number" >> $GITHUB_STEP_SUMMARY
|
||
echo "3. ✅ Merged dev → main branch" >> $GITHUB_STEP_SUMMARY
|
||
echo "4. ✅ Pushed to main (triggered production build)" >> $GITHUB_STEP_SUMMARY
|
||
echo "5. ✅ Waited for build completion (up to 3 hours)" >> $GITHUB_STEP_SUMMARY
|
||
echo "6. ✅ Created release tag ${{ needs.validate-inputs.outputs.new_version }} (after successful build)" >> $GITHUB_STEP_SUMMARY
|
||
echo "7. ✅ Created GitHub Release (DRAFT - edit manually)" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🏗️ Build Process:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 📋 **Quarto Build Container Workflow**: Triggered by main branch push" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 🔨 **Build Jobs**: HTML + PDF + EPUB generation on Linux (parallel builds)" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 📄 **PDF Processing**: Generated, compressed with Ghostscript, stored in build/pdf/" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 📦 **Artifacts**: main-html-linux (web content) + main-pdf-linux (PDF file) + main-epub-linux (EPUB file)" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 🔄 **Integration**: Downloaded all artifacts and combined into unified deployment" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 🌐 **Deployment**: Combined HTML + PDF + EPUB deployed to GitHub Pages (gh-pages branch)" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🌐 Access Your Published Textbook:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 📖 [Interactive Web Textbook](https://mlsysbook.ai/book/)" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 🔥 [TinyTorch Framework](https://mlsysbook.ai/tinytorch/)" >> $GITHUB_STEP_SUMMARY
|
||
echo "- ⚙️ [Hardware Kits](https://mlsysbook.ai/kits/)" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 📦 [Version Release Notes](https://github.com/${{ github.repository }}/releases/tag/${{ needs.validate-inputs.outputs.new_version }})" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 📄 [Download Complete PDF](https://github.com/${{ github.repository }}/releases/download/${{ needs.validate-inputs.outputs.new_version }}/Machine-Learning-Systems.pdf)" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 📚 [Download EPUB eBook](https://github.com/${{ github.repository }}/releases/download/${{ needs.validate-inputs.outputs.new_version }}/Machine-Learning-Systems.epub)" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 📄 [Direct PDF Access](https://mlsysbook.ai/book/assets/downloads/Machine-Learning-Systems.pdf)" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 📚 [Direct EPUB Access](https://mlsysbook.ai/book/assets/downloads/Machine-Learning-Systems.epub)" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 🎓 [Share with Students](https://mlsysbook.ai/book/)" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 📊 Build Status:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 📋 **Quarto Build Container Workflow**: Should be running/completed" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 🏗️ **Quarto Build**: HTML + PDF + EPUB generation" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 📄 **PDF Assets**: Available at `/assets/downloads/Machine-Learning-Systems.pdf`" >> $GITHUB_STEP_SUMMARY
|
||
echo "- 📚 **EPUB Assets**: Available at `/assets/downloads/Machine-Learning-Systems.epub`" >> $GITHUB_STEP_SUMMARY
|
||
|
||
cleanup-on-failure:
|
||
name: '🧹 Cleanup Failed Release'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
needs: [validate-inputs]
|
||
if: always() && github.event.inputs.confirm == 'PUBLISH' && (failure() || cancelled())
|
||
|
||
steps:
|
||
- name: 📥 Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 🧹 Clean up failed release artifacts
|
||
run: |
|
||
echo "🧹 Cleaning up artifacts from failed release..."
|
||
|
||
# Get the version that was being released
|
||
if [ -n "${{ needs.validate-inputs.outputs.new_version }}" ]; then
|
||
VERSION_TAG="${{ needs.validate-inputs.outputs.new_version }}"
|
||
echo "🎯 Cleaning up version: $VERSION_TAG"
|
||
else
|
||
echo "⚠️ No version information available, skipping cleanup"
|
||
exit 0
|
||
fi
|
||
|
||
# Configure git
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||
|
||
# Check and remove local tag
|
||
if git tag -l "$VERSION_TAG" | grep -q "$VERSION_TAG"; then
|
||
echo "🗑️ Removing local tag: $VERSION_TAG"
|
||
git tag -d "$VERSION_TAG"
|
||
else
|
||
echo "ℹ️ Local tag $VERSION_TAG does not exist"
|
||
fi
|
||
|
||
# Check and remove remote tag if it exists
|
||
if git ls-remote --tags origin | grep -q "refs/tags/$VERSION_TAG$"; then
|
||
echo "🗑️ Removing remote tag: $VERSION_TAG"
|
||
git push origin --delete "$VERSION_TAG" || echo "⚠️ Failed to delete remote tag (may not exist)"
|
||
else
|
||
echo "ℹ️ Remote tag $VERSION_TAG does not exist"
|
||
fi
|
||
|
||
# Check for any draft releases and delete them
|
||
echo "🔍 Checking for draft releases..."
|
||
DRAFT_RELEASE=$(curl -s \
|
||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||
-H "Accept: application/vnd.github.v3+json" \
|
||
"https://api.github.com/repos/${{ github.repository }}/releases" \
|
||
| jq -r ".[] | select(.tag_name == \"$VERSION_TAG\" and .draft == true) | .id")
|
||
|
||
if [ "$DRAFT_RELEASE" != "null" ] && [ -n "$DRAFT_RELEASE" ]; then
|
||
echo "🗑️ Deleting draft release: $DRAFT_RELEASE"
|
||
curl -s \
|
||
-X DELETE \
|
||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||
-H "Accept: application/vnd.github.v3+json" \
|
||
"https://api.github.com/repos/${{ github.repository }}/releases/$DRAFT_RELEASE"
|
||
echo "✅ Draft release deleted"
|
||
else
|
||
echo "ℹ️ No draft release found for $VERSION_TAG"
|
||
fi
|
||
|
||
echo "✅ Cleanup completed! Repository is ready for retry."
|
||
|
||
- name: 📊 Cleanup Summary
|
||
run: |
|
||
echo "## 🧹 Failed Release Cleanup Complete" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Version:** ${{ needs.validate-inputs.outputs.new_version || 'Unknown' }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Cleanup Time:** $(date)" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🔍 Original Inputs (for debugging):" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Description:** ${{ github.event.inputs.description }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Release Type:** ${{ github.event.inputs.release_type }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Dev Commit:** ${{ github.event.inputs.dev_commit }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Confirmation:** ${{ github.event.inputs.confirm }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **AI Generated Notes:** ${{ github.event.inputs.ai_generated_notes }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Status Check Timeout:** ${{ github.event.inputs.commit_status_timeout }} attempts" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Status Check Interval:** ${{ github.event.inputs.commit_status_interval }} seconds" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Triggered by:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Run ID:** ${{ github.run_id }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Run Attempt:** ${{ github.run_attempt }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🗑️ Cleaned Up:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- ✅ Local git tags removed" >> $GITHUB_STEP_SUMMARY
|
||
echo "- ✅ Remote git tags removed (if they existed)" >> $GITHUB_STEP_SUMMARY
|
||
echo "- ✅ Draft GitHub releases removed (if they existed)" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🔄 Ready for Retry:" >> $GITHUB_STEP_SUMMARY
|
||
echo "You can now safely re-run the publish workflow with the same version number." >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🐛 Troubleshooting:" >> $GITHUB_STEP_SUMMARY
|
||
echo "If you continue to have issues, check the workflow logs for the failed step." >> $GITHUB_STEP_SUMMARY
|
||
|
||
cleanup-on-timeout:
|
||
name: '⏰ Cleanup Timed Out Release'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
needs: [validate-inputs, trigger-production-build]
|
||
if: always() && github.event.inputs.confirm == 'PUBLISH' && (needs.trigger-production-build.result == 'failure' || needs.trigger-production-build.result == 'timeout')
|
||
|
||
steps:
|
||
- name: 📥 Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 🧹 Clean up timed out release artifacts
|
||
run: |
|
||
echo "⏰ Cleaning up artifacts from timed out release..."
|
||
|
||
# Get the version that was being released
|
||
if [ -n "${{ needs.validate-inputs.outputs.new_version }}" ]; then
|
||
VERSION_TAG="${{ needs.validate-inputs.outputs.new_version }}"
|
||
echo "🎯 Cleaning up version: $VERSION_TAG"
|
||
else
|
||
echo "⚠️ No version information available, skipping cleanup"
|
||
exit 0
|
||
fi
|
||
|
||
# Configure git
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||
|
||
# Check and remove local tag
|
||
if git tag -l "$VERSION_TAG" | grep -q "$VERSION_TAG"; then
|
||
echo "🗑️ Removing local tag: $VERSION_TAG"
|
||
git tag -d "$VERSION_TAG"
|
||
else
|
||
echo "ℹ️ Local tag $VERSION_TAG does not exist"
|
||
fi
|
||
|
||
# Check and remove remote tag if it exists
|
||
if git ls-remote --tags origin | grep -q "refs/tags/$VERSION_TAG$"; then
|
||
echo "🗑️ Removing remote tag: $VERSION_TAG"
|
||
git push origin --delete "$VERSION_TAG" || echo "⚠️ Failed to delete remote tag (may not exist)"
|
||
else
|
||
echo "ℹ️ Remote tag $VERSION_TAG does not exist"
|
||
fi
|
||
|
||
# Check for any draft releases and delete them
|
||
echo "🔍 Checking for draft releases..."
|
||
DRAFT_RELEASE=$(curl -s \
|
||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||
-H "Accept: application/vnd.github.v3+json" \
|
||
"https://api.github.com/repos/${{ github.repository }}/releases" \
|
||
| jq -r ".[] | select(.tag_name == \"$VERSION_TAG\" and .draft == true) | .id")
|
||
|
||
if [ "$DRAFT_RELEASE" != "null" ] && [ -n "$DRAFT_RELEASE" ]; then
|
||
echo "🗑️ Deleting draft release: $DRAFT_RELEASE"
|
||
curl -s \
|
||
-X DELETE \
|
||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||
-H "Accept: application/vnd.github.v3+json" \
|
||
"https://api.github.com/repos/${{ github.repository }}/releases/$DRAFT_RELEASE"
|
||
echo "✅ Draft release deleted"
|
||
else
|
||
echo "ℹ️ No draft release found for $VERSION_TAG"
|
||
fi
|
||
|
||
echo "✅ Cleanup completed! Repository is ready for retry."
|
||
|
||
- name: 🔄 Rollback Main Branch (if needed)
|
||
run: |
|
||
echo "🔄 Checking if main branch rollback is needed..."
|
||
|
||
# Only rollback if merge succeeded but later steps failed
|
||
if [ "${{ needs.merge-to-main.result }}" = "success" ] && [ "${{ needs.create-tag.result }}" != "success" ]; then
|
||
echo "⚠️ Merge succeeded but tag creation failed - considering rollback"
|
||
echo "🔍 Checking if main branch needs to be rolled back..."
|
||
|
||
# Get the commit before the merge
|
||
MERGE_COMMIT=$(git log --oneline -1 --grep="Release $VERSION_TAG" --format="%H" || echo "")
|
||
|
||
if [ -n "$MERGE_COMMIT" ]; then
|
||
PARENT_COMMIT=$(git log --format="%P" -n 1 "$MERGE_COMMIT" | cut -d' ' -f1)
|
||
echo "🔍 Found merge commit: $MERGE_COMMIT"
|
||
echo "🔍 Parent commit: $PARENT_COMMIT"
|
||
|
||
echo "⚠️ To manually rollback main branch, run:"
|
||
echo " git checkout main"
|
||
echo " git reset --hard $PARENT_COMMIT"
|
||
echo " git push origin main --force-with-lease"
|
||
echo ""
|
||
echo "⚠️ AUTOMATED ROLLBACK DISABLED - Manual intervention required"
|
||
echo "🛡️ This prevents accidental data loss"
|
||
else
|
||
echo "ℹ️ No merge commit found - no rollback needed"
|
||
fi
|
||
else
|
||
echo "ℹ️ No rollback needed - merge did not complete successfully"
|
||
fi
|
||
|
||
- name: 📊 Cleanup Summary
|
||
run: |
|
||
echo "## 🧹 Failed Release Cleanup Complete" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Version:** ${{ needs.validate-inputs.outputs.new_version || 'Unknown' }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Cleanup Time:** $(date)" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🔍 Original Inputs (for debugging):" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Description:** ${{ github.event.inputs.description }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Release Type:** ${{ github.event.inputs.release_type }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Dev Commit:** ${{ github.event.inputs.dev_commit }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Confirmation:** ${{ github.event.inputs.confirm }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **AI Generated Notes:** ${{ github.event.inputs.ai_generated_notes }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Status Check Timeout:** ${{ github.event.inputs.commit_status_timeout }} attempts" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Status Check Interval:** ${{ github.event.inputs.commit_status_interval }} seconds" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Triggered by:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Run ID:** ${{ github.run_id }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Run Attempt:** ${{ github.run_attempt }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🗑️ Cleaned Up:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- ✅ Local git tags removed" >> $GITHUB_STEP_SUMMARY
|
||
echo "- ✅ Remote git tags removed (if they existed)" >> $GITHUB_STEP_SUMMARY
|
||
echo "- ✅ Draft GitHub releases removed (if they existed)" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🔄 Ready for Retry:" >> $GITHUB_STEP_SUMMARY
|
||
echo "You can now safely re-run the publish workflow with the same version number." >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🐛 Troubleshooting:" >> $GITHUB_STEP_SUMMARY
|
||
echo "If you continue to have issues, check the workflow logs for the failed step." >> $GITHUB_STEP_SUMMARY
|
||
|
||
testing-mode-summary:
|
||
name: '🧪 Testing Mode Summary'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
if: github.event.inputs.confirm == 'PUBLISH' && github.event.inputs.testing_mode == 'yes'
|
||
needs: [validate-inputs, call-production-build]
|
||
|
||
steps:
|
||
- name: 🧪 Testing Mode Complete
|
||
run: |
|
||
echo "## 🧪 Testing Mode Complete" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Status:** ✅ Testing workflow completed successfully" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Mode:** Testing (no actual deployment)" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Triggered by:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Time:** $(date)" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 📋 What was tested:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- ✅ Input validation" >> $GITHUB_STEP_SUMMARY
|
||
echo "- ✅ Build workflow triggering" >> $GITHUB_STEP_SUMMARY
|
||
echo "- ✅ Build completion detection" >> $GITHUB_STEP_SUMMARY
|
||
echo "- ✅ Artifact availability" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🚀 To run actual deployment:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Set **testing_mode** to **no**" >> $GITHUB_STEP_SUMMARY
|
||
echo "- Re-run with same parameters" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🔍 Debug Information:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Run ID:** ${{ github.run_id }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Description:** ${{ github.event.inputs.description }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Release Type:** ${{ github.event.inputs.release_type }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Dev Commit:** ${{ github.event.inputs.dev_commit }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Concurrency Group:** publish-live-test-${{ github.run_number }}-${{ github.event.inputs.dev_commit }}" >> $GITHUB_STEP_SUMMARY
|
||
|
||
echo "✅ Testing mode completed successfully!"
|
||
echo "🧪 This run tested the workflow without actual deployment"
|
||
echo "🔄 Run ID: ${{ github.run_id }}"
|
||
echo "👤 Triggered by: ${{ github.actor }}"
|
||
echo "🌐 Branch: ${{ github.ref_name }}"
|
||
|
||
fail-validation:
|
||
name: '❌ Validation Failed'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
if: github.event.inputs.confirm != 'PUBLISH'
|
||
|
||
steps:
|
||
- name: ❌ Invalid confirmation
|
||
run: |
|
||
echo "## ❌ Publication Validation Failed" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Reason:** Invalid confirmation" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Expected:** PUBLISH" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Received:** ${{ github.event.inputs.confirm }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Triggered by:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "**Time:** $(date)" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### 🔍 Debug Information:" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Run ID:** ${{ github.run_id }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Description:** ${{ github.event.inputs.description }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Release Type:** ${{ github.event.inputs.release_type }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Dev Commit:** ${{ github.event.inputs.dev_commit }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **AI Generated Notes:** ${{ github.event.inputs.ai_generated_notes }}" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
|
||
echo "❌ Publication cancelled - invalid confirmation"
|
||
echo "🔒 You must type exactly 'PUBLISH' to confirm"
|
||
echo "📝 You entered: '${{ github.event.inputs.confirm }}'"
|
||
echo "👤 Triggered by: ${{ github.actor }}"
|
||
echo "🌐 Branch: ${{ github.ref_name }}"
|
||
echo "🔄 Run ID: ${{ github.run_id }}"
|
||
exit 1
|