Files
cs249r_book/.github/workflows/book-publish-live.yml
Vijay Janapa Reddi a76aab4676 ci: completely remove Windows build infrastructure
Per request, removed all traces of Windows container builds from the project.
This simplifies the CI pipeline to be Linux-only.

- Deleted `book/docker/windows/` directory and its Dockerfile
- Deleted `.github/workflows/infra-container-windows.yml`
- Removed Windows matrix jobs and steps from `book-build-container.yml`
- Removed Windows inputs and outputs from `book-build-container.yml`
- Removed Windows health checks from `infra-health-check.yml`
- Removed Windows references from `book-publish-live.yml`
- Removed Windows references from `book-validate-dev.yml`
2026-03-06 10:04:48 -05:00

2050 lines
93 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
#
# 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_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