mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-04-30 01:29:07 -05:00
Updates the book publishing workflow to conditionally verify downloaded artifacts based on the `deploy_target` input, preventing failures during partial deployments. Explicitly sets the output filenames for EPUB and PDF builds in Quarto configurations, ensuring consistent naming for generated book artifacts.
2054 lines
93 KiB
YAML
2054 lines
93 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 (vol1, vol2, all=everything)'
|
||
required: false
|
||
type: choice
|
||
options:
|
||
- 'all'
|
||
- '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@v6
|
||
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@v6
|
||
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@v6
|
||
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@v6
|
||
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@v6
|
||
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@v6
|
||
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: 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@v6
|
||
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@v6
|
||
with:
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 🔍 Validate API Contract
|
||
run: |
|
||
echo "🔍 Validating artifact names from API..."
|
||
VOL1_HTML="${{ needs.call-production-build.outputs.linux_html_vol1_artifact }}"
|
||
VOL2_HTML="${{ needs.call-production-build.outputs.linux_html_vol2_artifact }}"
|
||
echo "📘 Vol1 HTML artifact: '$VOL1_HTML'"
|
||
echo "📙 Vol2 HTML artifact: '$VOL2_HTML'"
|
||
if [ -z "$VOL1_HTML" ] || [ -z "$VOL2_HTML" ]; then
|
||
echo "❌ CRITICAL: Volume artifact names missing from build workflow outputs"
|
||
exit 1
|
||
fi
|
||
echo "✅ API contract validated for volume artifacts"
|
||
|
||
# =================================================================
|
||
# 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..."
|
||
DEPLOY_TARGET="${{ github.event.inputs.deploy_target }}"
|
||
if [[ "$DEPLOY_TARGET" == "vol1" || "$DEPLOY_TARGET" == "all" ]] && [ ! -d "html-vol1-temp" ]; then
|
||
echo "❌ Required Volume I HTML artifact was not downloaded."
|
||
exit 1
|
||
fi
|
||
if [[ "$DEPLOY_TARGET" == "vol2" || "$DEPLOY_TARGET" == "all" ]] && [ ! -d "html-vol2-temp" ]; then
|
||
echo "❌ Required Volume II HTML artifact was not downloaded."
|
||
exit 1
|
||
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/vol1/ - Volume I
|
||
# /book/vol2/ - Volume II
|
||
# /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 Volume I to /book/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 /book/vol1/..."
|
||
rm -rf book/vol1/
|
||
mkdir -p book/vol1
|
||
cp -r ../vol1-site/* book/vol1/
|
||
echo "✅ Volume I deployed to /book/vol1/"
|
||
else
|
||
echo "⚠️ Volume I site not found - skipping"
|
||
fi
|
||
fi
|
||
|
||
# =================================================================
|
||
# Deploy Volume II to /book/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 /book/vol2/..."
|
||
rm -rf book/vol2/
|
||
mkdir -p book/vol2
|
||
cp -r ../vol2-site/* book/vol2/
|
||
echo "✅ Volume II deployed to /book/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/vol1/"
|
||
echo '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="refresh" content="0;url=/book/vol1/"><link rel="canonical" href="https://mlsysbook.ai/book/vol1/"><title>Redirecting...</title></head><body><p>Redirecting to <a href="/book/vol1/">ML Systems Volume I</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/vol1/..."
|
||
echo '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="refresh" content="0;url=/book/vol1/"><link rel="canonical" href="https://mlsysbook.ai/book/vol1/"><title>Redirecting...</title></head><body><p>Redirecting to <a href="/book/vol1/">ML Systems Volume I</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@v6
|
||
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@v6
|
||
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@v6
|
||
with:
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 📄 Download PDF from previous job
|
||
uses: actions/download-artifact@v7
|
||
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@v6
|
||
with:
|
||
name: release-notes
|
||
path: release_notes_${{ needs.validate-inputs.outputs.new_version }}.md
|
||
|
||
- name: 📤 Upload Git Log Artifacts
|
||
uses: actions/upload-artifact@v6
|
||
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@v6
|
||
with:
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: 📄 Download PDF from previous job
|
||
uses: actions/download-artifact@v7
|
||
with:
|
||
name: pdf-artifact
|
||
path: ./
|
||
|
||
- name: 📚 Download EPUB from previous job
|
||
uses: actions/download-artifact@v7
|
||
with:
|
||
name: epub-artifact
|
||
path: ./
|
||
|
||
- name: 📝 Download release notes
|
||
uses: actions/download-artifact@v7
|
||
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@v6
|
||
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@v6
|
||
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
|