Files
cs249r_book/.github/workflows/book-build-container.yml
Vijay Janapa Reddi 1d3bcddd0d Harden remaining Windows PowerShell interpolations.
Replace subexpression-based OS/architecture logging in dockerized Windows compression steps with format strings to avoid escaping-related parse failures.
2026-03-06 08:06:16 -05:00

912 lines
42 KiB
YAML

name: '📚 Book · 🔨 Build (Container)'
# Concurrency disabled - allow unlimited parallel builds
# This workflow uses pre-built containers with all dependencies installed
# Matrix approach: 4 parallel jobs for maximum speed
on:
workflow_dispatch:
inputs:
build_linux:
description: '🐧 Build on Linux'
required: false
default: true
type: boolean
build_windows:
description: '🪟 Build on Windows'
required: false
default: true
type: boolean
build_html:
description: '📄 Build HTML format'
required: false
default: true
type: boolean
build_pdf:
description: '📑 Build PDF format'
required: false
default: true
type: boolean
build_epub:
description: '📚 Build EPUB format'
required: false
default: true
type: boolean
build_target:
description: '📦 Build target (vol1, vol2, all)'
required: false
type: choice
default: 'all'
options:
- vol1
- vol2
- all
target:
description: 'Target branch (dev/main)'
required: false
type: choice
default: 'dev'
options:
- dev
- main
container_registry:
description: 'Container registry (e.g., ghcr.io)'
required: false
type: string
default: 'ghcr.io'
container_tag:
description: 'Container tag (e.g., latest)'
required: false
type: string
default: 'latest'
workflow_call:
inputs:
build_linux:
required: false
type: boolean
default: true
build_windows:
required: false
type: boolean
default: true
build_html:
required: false
type: boolean
default: true
build_pdf:
required: false
type: boolean
default: true
build_epub:
required: false
type: boolean
default: true
build_target:
required: false
type: string
default: 'all'
target:
required: false
type: string
default: 'dev'
container_registry:
required: false
type: string
default: 'ghcr.io'
container_tag:
required: false
type: string
default: 'latest'
outputs:
build_success:
description: "Whether all builds completed successfully"
value: ${{ jobs.collect-outputs.outputs.build_success }}
build_target:
description: "Build target used (vol1/vol2/all)"
value: ${{ jobs.collect-outputs.outputs.build_target }}
failed_instances:
description: "Failed matrix build instances"
value: ${{ jobs.collect-outputs.outputs.failed_instances }}
cancelled_instances:
description: "Cancelled matrix build instances"
value: ${{ jobs.collect-outputs.outputs.cancelled_instances }}
# Volume I artifacts
linux_html_vol1_artifact:
description: "Linux HTML artifact name (Volume I)"
value: ${{ jobs.collect-outputs.outputs.linux_html_vol1_artifact }}
linux_pdf_vol1_artifact:
description: "Linux PDF artifact name (Volume I)"
value: ${{ jobs.collect-outputs.outputs.linux_pdf_vol1_artifact }}
linux_epub_vol1_artifact:
description: "Linux EPUB artifact name (Volume I)"
value: ${{ jobs.collect-outputs.outputs.linux_epub_vol1_artifact }}
# Volume II artifacts
linux_html_vol2_artifact:
description: "Linux HTML artifact name (Volume II)"
value: ${{ jobs.collect-outputs.outputs.linux_html_vol2_artifact }}
linux_pdf_vol2_artifact:
description: "Linux PDF artifact name (Volume II)"
value: ${{ jobs.collect-outputs.outputs.linux_pdf_vol2_artifact }}
linux_epub_vol2_artifact:
description: "Linux EPUB artifact name (Volume II)"
value: ${{ jobs.collect-outputs.outputs.linux_epub_vol2_artifact }}
# Windows Volume I artifacts
windows_html_vol1_artifact:
description: "Windows HTML artifact name (Volume I)"
value: ${{ jobs.collect-outputs.outputs.windows_html_vol1_artifact }}
windows_pdf_vol1_artifact:
description: "Windows PDF artifact name (Volume I)"
value: ${{ jobs.collect-outputs.outputs.windows_pdf_vol1_artifact }}
windows_epub_vol1_artifact:
description: "Windows EPUB artifact name (Volume I)"
value: ${{ jobs.collect-outputs.outputs.windows_epub_vol1_artifact }}
# Windows Volume II artifacts
windows_html_vol2_artifact:
description: "Windows HTML artifact name (Volume II)"
value: ${{ jobs.collect-outputs.outputs.windows_html_vol2_artifact }}
windows_pdf_vol2_artifact:
description: "Windows PDF artifact name (Volume II)"
value: ${{ jobs.collect-outputs.outputs.windows_pdf_vol2_artifact }}
windows_epub_vol2_artifact:
description: "Windows EPUB artifact name (Volume II)"
value: ${{ jobs.collect-outputs.outputs.windows_epub_vol2_artifact }}
permissions:
contents: read
packages: read
actions: read
# =============================================================================
# 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
jobs:
build:
name: '${{ matrix.platform_emoji }} Build ${{ matrix.platform_name }} · Vol ${{ matrix.volume == ''vol1'' && ''I'' || ''II'' }} (${{ matrix.format_emoji }} ${{ matrix.format_name }})'
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
# =================================================================
# Volume I builds
# =================================================================
- platform: linux
platform_name: Linux
platform_emoji: '🐧'
runner: ubuntu-latest
format_name: HTML
format_emoji: '📄'
volume: vol1
config: _quarto-html-vol1.yml
render_target: html
enabled: ${{ inputs.build_linux && inputs.build_html && (inputs.build_target == 'vol1' || inputs.build_target == 'all') }}
artifact_name: ${{ inputs.target }}-html-vol1-linux
output_dir: _build/html-vol1
- platform: linux
platform_name: Linux
platform_emoji: '🐧'
runner: ubuntu-latest
format_name: PDF
format_emoji: '📑'
volume: vol1
config: _quarto-pdf-vol1.yml
render_target: titlepage-pdf
enabled: ${{ inputs.build_linux && inputs.build_pdf && (inputs.build_target == 'vol1' || inputs.build_target == 'all') }}
artifact_name: ${{ inputs.target }}-pdf-vol1-linux
output_dir: _build/pdf-vol1
- platform: linux
platform_name: Linux
platform_emoji: '🐧'
runner: ubuntu-latest
format_name: EPUB
format_emoji: '📚'
volume: vol1
config: _quarto-epub-vol1.yml
render_target: epub
enabled: ${{ inputs.build_linux && inputs.build_epub && (inputs.build_target == 'vol1' || inputs.build_target == 'all') }}
artifact_name: ${{ inputs.target }}-epub-vol1-linux
output_dir: _build/epub-vol1
# =================================================================
# Volume II builds
# =================================================================
- platform: linux
platform_name: Linux
platform_emoji: '🐧'
runner: ubuntu-latest
format_name: HTML
format_emoji: '📄'
volume: vol2
config: _quarto-html-vol2.yml
render_target: html
enabled: ${{ inputs.build_linux && inputs.build_html && (inputs.build_target == 'vol2' || inputs.build_target == 'all') }}
artifact_name: ${{ inputs.target }}-html-vol2-linux
output_dir: _build/html-vol2
- platform: linux
platform_name: Linux
platform_emoji: '🐧'
runner: ubuntu-latest
format_name: PDF
format_emoji: '📑'
volume: vol2
config: _quarto-pdf-vol2.yml
render_target: titlepage-pdf
enabled: ${{ inputs.build_linux && inputs.build_pdf && (inputs.build_target == 'vol2' || inputs.build_target == 'all') }}
artifact_name: ${{ inputs.target }}-pdf-vol2-linux
output_dir: _build/pdf-vol2
- platform: linux
platform_name: Linux
platform_emoji: '🐧'
runner: ubuntu-latest
format_name: EPUB
format_emoji: '📚'
volume: vol2
config: _quarto-epub-vol2.yml
render_target: epub
enabled: ${{ inputs.build_linux && inputs.build_epub && (inputs.build_target == 'vol2' || inputs.build_target == 'all') }}
artifact_name: ${{ inputs.target }}-epub-vol2-linux
output_dir: _build/epub-vol2
# =================================================================
# Volume I builds (Windows)
# =================================================================
- platform: windows
platform_name: Windows
platform_emoji: '🪟'
runner: windows-latest
format_name: HTML
format_emoji: '📄'
volume: vol1
config: _quarto-html-vol1.yml
render_target: html
enabled: ${{ inputs.build_windows && inputs.build_html && (inputs.build_target == 'vol1' || inputs.build_target == 'all') }}
artifact_name: ${{ inputs.target }}-html-vol1-windows
output_dir: _build/html-vol1
- platform: windows
platform_name: Windows
platform_emoji: '🪟'
runner: windows-latest
format_name: PDF
format_emoji: '📑'
volume: vol1
config: _quarto-pdf-vol1.yml
render_target: titlepage-pdf
enabled: ${{ inputs.build_windows && inputs.build_pdf && (inputs.build_target == 'vol1' || inputs.build_target == 'all') }}
artifact_name: ${{ inputs.target }}-pdf-vol1-windows
output_dir: _build/pdf-vol1
- platform: windows
platform_name: Windows
platform_emoji: '🪟'
runner: windows-latest
format_name: EPUB
format_emoji: '📚'
volume: vol1
config: _quarto-epub-vol1.yml
render_target: epub
enabled: ${{ inputs.build_windows && inputs.build_epub && (inputs.build_target == 'vol1' || inputs.build_target == 'all') }}
artifact_name: ${{ inputs.target }}-epub-vol1-windows
output_dir: _build/epub-vol1
# =================================================================
# Volume II builds (Windows)
# =================================================================
- platform: windows
platform_name: Windows
platform_emoji: '🪟'
runner: windows-latest
format_name: HTML
format_emoji: '📄'
volume: vol2
config: _quarto-html-vol2.yml
render_target: html
enabled: ${{ inputs.build_windows && inputs.build_html && (inputs.build_target == 'vol2' || inputs.build_target == 'all') }}
artifact_name: ${{ inputs.target }}-html-vol2-windows
output_dir: _build/html-vol2
- platform: windows
platform_name: Windows
platform_emoji: '🪟'
runner: windows-latest
format_name: PDF
format_emoji: '📑'
volume: vol2
config: _quarto-pdf-vol2.yml
render_target: titlepage-pdf
enabled: ${{ inputs.build_windows && inputs.build_pdf && (inputs.build_target == 'vol2' || inputs.build_target == 'all') }}
artifact_name: ${{ inputs.target }}-pdf-vol2-windows
output_dir: _build/pdf-vol2
- platform: windows
platform_name: Windows
platform_emoji: '🪟'
runner: windows-latest
format_name: EPUB
format_emoji: '📚'
volume: vol2
config: _quarto-epub-vol2.yml
render_target: epub
enabled: ${{ inputs.build_windows && inputs.build_epub && (inputs.build_target == 'vol2' || inputs.build_target == 'all') }}
artifact_name: ${{ inputs.target }}-epub-vol2-windows
output_dir: _build/epub-vol2
outputs:
platform: ${{ matrix.platform }}
format: ${{ matrix.format_name }}
artifact_name: ${{ matrix.artifact_name }}
output_dir: ${{ matrix.output_dir }}
# Only Linux runs in containers
container: ${{ matrix.platform == 'linux' && format('{0}/{1}/quarto-{2}:{3}', inputs.container_registry || 'ghcr.io', github.repository, matrix.platform, inputs.container_tag || 'latest') || null }}
env:
CONTAINER_IMAGE: ${{ format('{0}/{1}/quarto-{2}:{3}', inputs.container_registry || 'ghcr.io', github.repository, matrix.platform, inputs.container_tag || 'latest') }}
# Using vars.BOOK_DOCKER (repository variable) - works in all contexts
DOCKERFILE_PATH: ./${{ vars.BOOK_DOCKER }}/${{ matrix.platform }}/Dockerfile
steps:
- name: 🛑 Skip build
if: "!matrix.enabled"
run: echo "Build skipped because matrix.enabled is false"
- name: 📥 Checkout repository
if: matrix.enabled
uses: actions/checkout@v6
with:
ref: ${{ inputs.target }}
fetch-depth: 0
- name: 🔍 Debug build configuration
if: matrix.enabled
run: |
echo "🎯 Target branch: ${{ inputs.target }}"
echo "🐧 Build Linux: ${{ inputs.build_linux }}"
echo "🪟 Build Windows: ${{ inputs.build_windows }}"
echo "📄 Build HTML: ${{ inputs.build_html }}"
echo "📑 Build PDF: ${{ inputs.build_pdf }}"
echo "📚 Build EPUB: ${{ inputs.build_epub }}"
echo "🐳 Container registry: ${{ inputs.container_registry }}"
echo "🏷️ Container tag: ${{ inputs.container_tag }}"
echo "✅ Current matrix job enabled: ${{ matrix.enabled }}"
- name: 🔑 Log in to GitHub Container Registry
if: matrix.platform == 'windows' && matrix.enabled
uses: docker/login-action@v3
with:
registry: ${{ inputs.container_registry }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 🐳 Ensure Docker daemon (Windows)
if: matrix.platform == 'windows' && matrix.enabled
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$dockerService = Get-Service -Name docker -ErrorAction Stop
if ($dockerService.Status -ne 'Running') {
Write-Host "Starting Docker service..."
Start-Service -Name docker
} else {
Write-Host "Docker service already running."
}
$maxAttempts = 12
for ($i = 1; $i -le $maxAttempts; $i++) {
try {
docker version | Out-Null
Write-Host "Docker daemon is ready."
break
} catch {
if ($i -eq $maxAttempts) {
throw "Docker daemon failed to start after waiting."
}
Start-Sleep -Seconds 5
}
}
- name: 🐳 Pull Docker Image
if: matrix.platform == 'windows' && matrix.enabled
run: docker pull ${{ env.CONTAINER_IMAGE }}
- name: 🧪 Preflight toolchain (Linux)
if: matrix.platform == 'linux' && matrix.enabled
working-directory: ${{ vars.BOOK_QUARTO }}
env:
PYTHONPATH: ${{ github.workspace }}
run: |
set -eu
echo "🧪 Running Linux toolchain preflight checks..."
run_check() {
CHECK_NAME="$1"
CHECK_CMD="$2"
echo "▶ preflight: $CHECK_NAME"
if sh -c "$CHECK_CMD"; then
echo "✅ preflight: $CHECK_NAME"
else
echo "❌ preflight failed: $CHECK_NAME"
exit 1
fi
}
run_check "quarto on PATH" "command -v quarto >/dev/null"
run_check "quarto version" "quarto --version | sed -n '1p'"
run_check "pandoc available" "command -v pandoc >/dev/null || quarto pandoc --version >/dev/null 2>&1"
run_check "pandoc version" "if command -v pandoc >/dev/null; then pandoc --version | sed -n '1p'; else quarto pandoc --version | sed -n '1p'; fi"
run_check "python3 on PATH" "command -v python3 >/dev/null"
run_check "python3 version" "python3 --version"
run_check "mlsysim import" "python3 -c \"import mlsysim,sys; print('mlsysim:', mlsysim.__file__); print('python:', sys.executable)\""
run_check "Rscript on PATH" "command -v Rscript >/dev/null"
run_check "Rscript version" "Rscript --version | sed -n '1p'"
run_check "inkscape on PATH" "command -v inkscape >/dev/null"
run_check "inkscape version" "inkscape --version | sed -n '1p'"
if [ "${{ matrix.format_name }}" = "PDF" ]; then
run_check "lualatex on PATH" "command -v lualatex >/dev/null"
run_check "lualatex version" "lualatex --version | sed -n '1p'"
run_check "ghostscript (gs) on PATH" "command -v gs >/dev/null"
run_check "ghostscript version" "gs --version"
fi
if [ "${{ matrix.format_name }}" = "EPUB" ]; then
run_check "Pillow import" "python3 -c \"import PIL; print('Pillow:', PIL.__version__)\""
fi
echo "✅ Linux toolchain preflight checks passed"
- name: 🧪 Preflight toolchain (Windows)
if: matrix.platform == 'windows' && matrix.enabled
shell: pwsh
run: |
Write-Host "🧪 Running Windows toolchain preflight checks..."
docker run --rm -e PYTHONPATH=C:\workspace -v "$($PWD.Path):C:\workspace" -w "C:\workspace\book\quarto" ${{ env.CONTAINER_IMAGE }} pwsh -NoLogo -Command "
`$ErrorActionPreference = 'Stop'
`$PSNativeCommandUseErrorActionPreference = `$true
function Invoke-Check {
param(
[string]`$Name,
[scriptblock]`$Action
)
Write-Host \"▶ preflight: `$Name\"
try {
& `$Action
if (-not `$?) { throw 'Command returned non-zero status' }
Write-Host \"✅ preflight: `$Name\"
} catch {
`$errorMessage = `$_.Exception.Message
Write-Error (\"❌ preflight failed: `$Name -- {0}\" -f `$errorMessage)
throw
}
}
Invoke-Check 'quarto on PATH' { `$resolved = Get-Command quarto -ErrorAction Stop; Write-Host (\" quarto -> {0}\" -f `$resolved.Source) }
Invoke-Check 'quarto version' { quarto --version | Select-Object -First 1 }
Invoke-Check 'pandoc available' {
`$pandocCmd = Get-Command pandoc -ErrorAction SilentlyContinue
if (`$pandocCmd) {
Write-Host (\" pandoc -> {0}\" -f `$pandocCmd.Source)
} else {
quarto pandoc --version | Select-Object -First 1 | Out-Null
Write-Host \" pandoc -> bundled via quarto\"
}
}
Invoke-Check 'pandoc version' {
`$pandocCmd = Get-Command pandoc -ErrorAction SilentlyContinue
if (`$pandocCmd) {
pandoc --version | Select-Object -First 1
} else {
quarto pandoc --version | Select-Object -First 1
}
}
Invoke-Check 'python on PATH' { `$resolved = Get-Command python -ErrorAction Stop; Write-Host (\" python -> {0}\" -f `$resolved.Source) }
Invoke-Check 'python version' { python --version }
Invoke-Check 'python3 on PATH' { `$resolved = Get-Command python3 -ErrorAction Stop; Write-Host (\" python3 -> {0}\" -f `$resolved.Source) }
Invoke-Check 'python3 version' { python3 --version }
Invoke-Check 'mlsysim import' { python -c 'import mlsysim,sys; print(\"mlsysim:\", mlsysim.__file__); print(\"python:\", sys.executable)' }
Invoke-Check 'Rscript on PATH' { `$resolved = Get-Command Rscript -ErrorAction Stop; Write-Host (\" Rscript -> {0}\" -f `$resolved.Source) }
Invoke-Check 'Rscript version' { Rscript --version | Select-Object -First 1 }
Invoke-Check 'inkscape on PATH' { `$resolved = Get-Command inkscape -ErrorAction Stop; Write-Host (\" inkscape -> {0}\" -f `$resolved.Source) }
Invoke-Check 'inkscape version' { inkscape --version | Select-Object -First 1 }
if ('${{ matrix.format_name }}' -eq 'PDF') {
Invoke-Check 'lualatex on PATH' { `$resolved = Get-Command lualatex -ErrorAction Stop; Write-Host (\" lualatex -> {0}\" -f `$resolved.Source) }
Invoke-Check 'lualatex version' { lualatex --version | Select-Object -First 1 }
Invoke-Check 'ghostscript on PATH' {
`$gsCmd = Get-Command gs -ErrorAction SilentlyContinue
if (-not `$gsCmd) { `$gsCmd = Get-Command gswin64c -ErrorAction SilentlyContinue }
if (-not `$gsCmd) { throw 'Ghostscript not found (expected gs or gswin64c)' }
Write-Host (\" ghostscript -> {0}\" -f `$gsCmd.Source)
}
Invoke-Check 'ghostscript version' {
`$gsCmd = Get-Command gs -ErrorAction SilentlyContinue
if (-not `$gsCmd) { `$gsCmd = Get-Command gswin64c -ErrorAction SilentlyContinue }
& `$gsCmd.Source --version
}
}
if ('${{ matrix.format_name }}' -eq 'EPUB') {
Invoke-Check 'Pillow import' { python -c 'import PIL; print(\"Pillow:\", PIL.__version__)' }
}
"
Write-Host "✅ Windows toolchain preflight checks passed"
- name: 🔨 Build ${{ matrix.format_name }} (Linux)
if: matrix.platform == 'linux' && matrix.enabled
working-directory: ${{ vars.BOOK_QUARTO }}
env:
PYTHONPATH: ${{ github.workspace }}
run: |
echo "🔨 Building ${{ matrix.format_name }} on Linux container..."
rm -f _quarto.yml
cp config/${{ matrix.config }} _quarto.yml
quarto render --to ${{ matrix.render_target }} --output-dir "${{ matrix.output_dir }}"
echo "✅ ${{ matrix.format_name }} build completed"
- name: 🔨 Build ${{ matrix.format_name }} (Windows)
if: matrix.platform == 'windows' && matrix.enabled
shell: pwsh
run: |
Write-Host "🔨 Building ${{ matrix.format_name }} on Windows container..."
docker run --rm -e PYTHONPATH=C:\workspace -v "$($PWD.Path):C:\workspace" -w "C:\workspace\book\quarto" ${{ env.CONTAINER_IMAGE }} pwsh -NoLogo -Command "
if (Test-Path '_quarto.yml') { Remove-Item '_quarto.yml' -Force }
Copy-Item 'config\${{ matrix.config }}' '_quarto.yml' -Force
quarto render --to ${{ matrix.render_target }} --output-dir '${{ matrix.output_dir }}'
"
Write-Host "✅ ${{ matrix.format_name }} build completed"
- name: 📉 Compress PDF (Linux)
if: matrix.platform == 'linux' && matrix.format_name == 'PDF' && matrix.enabled
working-directory: ${{ vars.BOOK_QUARTO }}/${{ matrix.output_dir }}
run: |
if [ -f "Machine-Learning-Systems.pdf" ]; then
echo "📉 Compressing PDF with professional compression tool..."
echo "🔍 DEBUG: PWD=$(pwd)"
echo "🔍 DEBUG: Checking for compress script:"
ls -la ../../publish/compress_pdf.py || echo "❌ Script not found at ../../publish/"
# Use relative path from current working directory (book/quarto/_build/pdf)
SCRIPT_PATH="../../publish/compress_pdf.py"
if [ -f "$SCRIPT_PATH" ]; then
echo "✅ Using script at: $SCRIPT_PATH"
python3 "$SCRIPT_PATH" \
--input "Machine-Learning-Systems.pdf" \
--output "compressed.pdf" \
--quality minimal \
--verbose
else
# Fallback to absolute path via github.workspace
SCRIPT_PATH="${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/compress_pdf.py"
echo "🔄 Trying fallback path: $SCRIPT_PATH"
python3 "$SCRIPT_PATH" \
--input "Machine-Learning-Systems.pdf" \
--output "compressed.pdf" \
--quality minimal \
--verbose
fi
mv compressed.pdf Machine-Learning-Systems.pdf
echo "✅ PDF compression completed"
else
echo "⚠️ PDF file not found for compression"
fi
- name: 📉 Compress PDF (Windows)
if: matrix.platform == 'windows' && matrix.format_name == 'PDF' && matrix.enabled
shell: pwsh
run: |
Write-Host "🔨 Compressing PDF on Windows container..."
docker run --rm -e PYTHONPATH=C:\workspace -v "$($PWD.Path):C:\workspace" -w "C:\workspace\book\quarto\${{ matrix.output_dir }}" ${{ env.CONTAINER_IMAGE }} pwsh -NoLogo -Command "
if (Test-Path 'Machine-Learning-Systems.pdf') {
Write-Host '📉 Compressing PDF with professional compression tool...'
Write-Host '🔍 DEBUG: Current directory:'
Get-Location
Write-Host '🔍 DEBUG: Windows environment info:'
Write-Host (\"OS: {0}\" -f [System.Environment]::OSVersion.VersionString)
Write-Host (\"Architecture: {0}\" -f [System.Environment]::Is64BitOperatingSystem)
Write-Host '🔍 DEBUG: Verifying Python and Pillow installation:'
`$py_version = (python --version 2>&1).Trim()
Write-Host \" 🐍 Python Version: `$py_version\"
`$py_path = (python -c 'import sys; print(sys.executable)').Trim()
Write-Host \" 🐍 Python Path: `$py_path\"
`$pillow_version = (python -c 'import PIL; print(PIL.__version__)').Trim()
Write-Host \" ✅ Pillow Version: `$pillow_version\"
Write-Host '🔍 DEBUG: Checking for script at relative path:'
if (Test-Path '..\\..\\publish\\compress_pdf.py') {
Write-Host '✅ Found script at ..\\..\\publish\\compress_pdf.py'
python ..\..\publish\compress_pdf.py --input 'Machine-Learning-Systems.pdf' --output 'compressed.pdf' --quality minimal --verbose
} else {
Write-Host '🔄 Trying fallback path: C:\workspace\book\quarto\publish\compress_pdf.py'
python C:\workspace\book\quarto\publish\compress_pdf.py --input 'Machine-Learning-Systems.pdf' --output 'compressed.pdf' --quality minimal --verbose
}
if (Test-Path 'compressed.pdf') {
Move-Item -Force 'compressed.pdf' 'Machine-Learning-Systems.pdf'
Write-Host '✅ PDF compression completed'
}
} else {
Write-Warning '⚠️ Machine-Learning-Systems.pdf not found for compression.'
}
"
Write-Host "✅ PDF compression completed."
- name: 📚 Compress EPUB (Linux)
if: matrix.platform == 'linux' && matrix.format_name == 'EPUB' && matrix.enabled
working-directory: ${{ vars.BOOK_QUARTO }}/${{ matrix.output_dir }}
run: |
if [ -f "Machine-Learning-Systems.epub" ]; then
echo "📚 Compressing EPUB with optimized compression tool..."
echo "🔍 DEBUG: PWD=$(pwd)"
echo "🔍 DEBUG: Repository structure from current directory:"
ls -la ../../ | head -10
echo "🔍 DEBUG: Checking for compress script:"
ls -la ../../publish/compress_epub.py || echo "❌ Script not found at ../../publish/"
echo "🔍 DEBUG: Checking fallback path:"
ls -la "${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/compress_epub.py" || echo "❌ Script not found at github.workspace path"
# Use relative path from current working directory (book/quarto/_build/epub)
SCRIPT_PATH="../../publish/compress_epub.py"
if [ -f "$SCRIPT_PATH" ]; then
echo "✅ Using script at: $SCRIPT_PATH"
python3 "$SCRIPT_PATH" \
--input "Machine-Learning-Systems.epub" \
--output "compressed.epub" \
--verbose
else
# Fallback to absolute path via github.workspace
SCRIPT_PATH="${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/compress_epub.py"
echo "🔄 Trying fallback path: $SCRIPT_PATH"
python3 "$SCRIPT_PATH" \
--input "Machine-Learning-Systems.epub" \
--output "compressed.epub" \
--verbose
fi
mv compressed.epub Machine-Learning-Systems.epub
echo "✅ EPUB compression completed (using optimized defaults: quality=50, max-size=1000px)"
else
echo "⚠️ EPUB file not found for compression"
fi
- name: 📚 Compress EPUB (Windows)
if: matrix.platform == 'windows' && matrix.format_name == 'EPUB' && matrix.enabled
shell: pwsh
run: |
Write-Host "🔨 Compressing EPUB on Windows container..."
docker run --rm -e PYTHONPATH=C:\workspace -v "$($PWD.Path):C:\workspace" -w "C:\workspace\book\quarto\${{ matrix.output_dir }}" ${{ env.CONTAINER_IMAGE }} pwsh -NoLogo -Command "
if (Test-Path 'Machine-Learning-Systems.epub') {
Write-Host '📚 Compressing EPUB with optimized compression tool...'
Write-Host '🔍 DEBUG: Current directory:'
Get-Location
Write-Host '🔍 DEBUG: Windows environment info:'
Write-Host (\"OS: {0}\" -f [System.Environment]::OSVersion.VersionString)
Write-Host (\"Architecture: {0}\" -f [System.Environment]::Is64BitOperatingSystem)
Write-Host '🔍 DEBUG: Verifying Python and Pillow installation:'
`$py_version = (python --version 2>&1).Trim()
Write-Host \" 🐍 Python Version: `$py_version\"
`$py_path = (python -c 'import sys; print(sys.executable)').Trim()
Write-Host \" 🐍 Python Path: `$py_path\"
`$pillow_version = (python -c 'import PIL; print(PIL.__version__)').Trim()
Write-Host \" ✅ Pillow Version: `$pillow_version\"
Write-Host '🔍 DEBUG: Checking for script at relative path:'
if (Test-Path '..\\..\\publish\\compress_epub.py') {
Write-Host '✅ Found script at ..\\..\\publish\\compress_epub.py'
python ..\..\publish\compress_epub.py --input 'Machine-Learning-Systems.epub' --output 'compressed.epub' --verbose
} else {
Write-Host '🔄 Trying fallback path: C:\workspace\book\quarto\publish\compress_epub.py'
python C:\workspace\book\quarto\publish\compress_epub.py --input 'Machine-Learning-Systems.epub' --output 'compressed.epub' --verbose
}
if (Test-Path 'compressed.epub') {
Move-Item -Force 'compressed.epub' 'Machine-Learning-Systems.epub'
Write-Host '✅ EPUB compression completed (using optimized defaults: quality=50, max-size=1000px)'
}
} else {
Write-Warning '⚠️ Machine-Learning-Systems.epub not found for compression.'
}
"
Write-Host "✅ EPUB compression completed."
- name: 📤 Upload artifact
uses: actions/upload-artifact@v6
if: matrix.enabled
with:
name: ${{ matrix.artifact_name }}
path: ${{ vars.BOOK_QUARTO }}/${{ matrix.output_dir }}
collect-outputs:
name: '📊 Collect Outputs'
needs: build
runs-on: ubuntu-latest
if: always()
outputs:
build_success: ${{ steps.collect.outputs.build_success }}
build_target: ${{ steps.collect.outputs.build_target }}
failed_instances: ${{ steps.matrix-status.outputs.failed_instances }}
cancelled_instances: ${{ steps.matrix-status.outputs.cancelled_instances }}
# Volume I artifacts
linux_html_vol1_artifact: ${{ steps.collect.outputs.linux_html_vol1_artifact }}
linux_pdf_vol1_artifact: ${{ steps.collect.outputs.linux_pdf_vol1_artifact }}
linux_epub_vol1_artifact: ${{ steps.collect.outputs.linux_epub_vol1_artifact }}
# Volume II artifacts
linux_html_vol2_artifact: ${{ steps.collect.outputs.linux_html_vol2_artifact }}
linux_pdf_vol2_artifact: ${{ steps.collect.outputs.linux_pdf_vol2_artifact }}
linux_epub_vol2_artifact: ${{ steps.collect.outputs.linux_epub_vol2_artifact }}
# Windows Volume I artifacts
windows_html_vol1_artifact: ${{ steps.collect.outputs.windows_html_vol1_artifact }}
windows_pdf_vol1_artifact: ${{ steps.collect.outputs.windows_pdf_vol1_artifact }}
windows_epub_vol1_artifact: ${{ steps.collect.outputs.windows_epub_vol1_artifact }}
# Windows Volume II artifacts
windows_html_vol2_artifact: ${{ steps.collect.outputs.windows_html_vol2_artifact }}
windows_pdf_vol2_artifact: ${{ steps.collect.outputs.windows_pdf_vol2_artifact }}
windows_epub_vol2_artifact: ${{ steps.collect.outputs.windows_epub_vol2_artifact }}
steps:
- name: 🔎 Collect matrix job status details
id: matrix-status
uses: actions/github-script@v7
with:
script: |
const runId = context.runId;
const { owner, repo } = context.repo;
const allJobs = [];
for (let page = 1; ; page++) {
const resp = await github.rest.actions.listJobsForWorkflowRun({
owner,
repo,
run_id: runId,
per_page: 100,
page,
});
allJobs.push(...resp.data.jobs);
if (resp.data.jobs.length < 100) break;
}
const matrixBuildJobs = allJobs.filter((job) => {
return job.name.includes(" Build ") && job.name.includes("Vol ");
});
const failedJobs = matrixBuildJobs.filter((job) =>
["failure", "timed_out", "action_required", "stale"].includes(job.conclusion || "")
);
const cancelledJobs = matrixBuildJobs.filter((job) =>
(job.conclusion || "") === "cancelled"
);
const failedNames = failedJobs.map((job) => job.name);
const cancelledNames = cancelledJobs.map((job) => job.name);
core.info(`Detected ${failedNames.length} failed matrix instance(s).`);
core.info(`Detected ${cancelledNames.length} cancelled matrix instance(s).`);
core.setOutput("failed_instances", failedNames.join(" | "));
core.setOutput("cancelled_instances", cancelledNames.join(" | "));
- name: 📊 Collect results
id: collect
run: |
# Determine overall build success
if [[ "${{ needs.build.result }}" == "success" || "${{ needs.build.result }}" == "skipped" ]]; then
echo "build_success=true" >> $GITHUB_OUTPUT
BUILD_SUCCESS_MSG="✅ Success"
else
echo "build_success=false" >> $GITHUB_OUTPUT
BUILD_SUCCESS_MSG="❌ Failure"
fi
FAILED_INSTANCES="${{ steps.matrix-status.outputs.failed_instances }}"
CANCELLED_INSTANCES="${{ steps.matrix-status.outputs.cancelled_instances }}"
if [[ -n "$FAILED_INSTANCES" ]]; then
echo "❌ Failed matrix instances: $FAILED_INSTANCES"
fi
if [[ -n "$CANCELLED_INSTANCES" ]]; then
echo "⚠️ Cancelled matrix instances (often fail-fast cascade): $CANCELLED_INSTANCES"
fi
{
echo "## Container Matrix Result"
echo "- Overall: $BUILD_SUCCESS_MSG"
if [[ -n "$FAILED_INSTANCES" ]]; then
echo "- Failed instances: $FAILED_INSTANCES"
fi
if [[ -n "$CANCELLED_INSTANCES" ]]; then
echo "- Cancelled instances: $CANCELLED_INSTANCES"
fi
} >> "$GITHUB_STEP_SUMMARY"
# 🔌 API-style artifact name generation (reliable and predictable)
TARGET="${{ inputs.target }}"
BUILD_TARGET="${{ inputs.build_target }}"
echo "🔌 Generating artifact names for target: $TARGET, build_target: $BUILD_TARGET"
echo "build_target=$BUILD_TARGET" >> $GITHUB_OUTPUT
# =================================================================
# Volume I artifacts
# =================================================================
if [[ "$BUILD_TARGET" == "vol1" || "$BUILD_TARGET" == "all" ]]; then
if [[ "${{ inputs.build_linux && inputs.build_html }}" == "true" ]]; then
ARTIFACT_NAME="${TARGET}-html-vol1-linux"
echo "linux_html_vol1_artifact=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
echo "📄 Linux HTML artifact (Vol I): $ARTIFACT_NAME"
fi
if [[ "${{ inputs.build_linux && inputs.build_pdf }}" == "true" ]]; then
ARTIFACT_NAME="${TARGET}-pdf-vol1-linux"
echo "linux_pdf_vol1_artifact=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
echo "📑 Linux PDF artifact (Vol I): $ARTIFACT_NAME"
fi
if [[ "${{ inputs.build_linux && inputs.build_epub }}" == "true" ]]; then
ARTIFACT_NAME="${TARGET}-epub-vol1-linux"
echo "linux_epub_vol1_artifact=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
echo "📚 Linux EPUB artifact (Vol I): $ARTIFACT_NAME"
fi
fi
# =================================================================
# Volume II artifacts
# =================================================================
if [[ "$BUILD_TARGET" == "vol2" || "$BUILD_TARGET" == "all" ]]; then
if [[ "${{ inputs.build_linux && inputs.build_html }}" == "true" ]]; then
ARTIFACT_NAME="${TARGET}-html-vol2-linux"
echo "linux_html_vol2_artifact=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
echo "📄 Linux HTML artifact (Vol II): $ARTIFACT_NAME"
fi
if [[ "${{ inputs.build_linux && inputs.build_pdf }}" == "true" ]]; then
ARTIFACT_NAME="${TARGET}-pdf-vol2-linux"
echo "linux_pdf_vol2_artifact=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
echo "📑 Linux PDF artifact (Vol II): $ARTIFACT_NAME"
fi
if [[ "${{ inputs.build_linux && inputs.build_epub }}" == "true" ]]; then
ARTIFACT_NAME="${TARGET}-epub-vol2-linux"
echo "linux_epub_vol2_artifact=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
echo "📚 Linux EPUB artifact (Vol II): $ARTIFACT_NAME"
fi
fi
# =================================================================
# Windows Volume I artifacts
# =================================================================
if [[ "$BUILD_TARGET" == "vol1" || "$BUILD_TARGET" == "all" ]]; then
if [[ "${{ inputs.build_windows && inputs.build_html }}" == "true" ]]; then
ARTIFACT_NAME="${TARGET}-html-vol1-windows"
echo "windows_html_vol1_artifact=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
echo "📄 Windows HTML artifact (Vol I): $ARTIFACT_NAME"
fi
if [[ "${{ inputs.build_windows && inputs.build_pdf }}" == "true" ]]; then
ARTIFACT_NAME="${TARGET}-pdf-vol1-windows"
echo "windows_pdf_vol1_artifact=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
echo "📑 Windows PDF artifact (Vol I): $ARTIFACT_NAME"
fi
if [[ "${{ inputs.build_windows && inputs.build_epub }}" == "true" ]]; then
ARTIFACT_NAME="${TARGET}-epub-vol1-windows"
echo "windows_epub_vol1_artifact=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
echo "📚 Windows EPUB artifact (Vol I): $ARTIFACT_NAME"
fi
fi
# =================================================================
# Windows Volume II artifacts
# =================================================================
if [[ "$BUILD_TARGET" == "vol2" || "$BUILD_TARGET" == "all" ]]; then
if [[ "${{ inputs.build_windows && inputs.build_html }}" == "true" ]]; then
ARTIFACT_NAME="${TARGET}-html-vol2-windows"
echo "windows_html_vol2_artifact=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
echo "📄 Windows HTML artifact (Vol II): $ARTIFACT_NAME"
fi
if [[ "${{ inputs.build_windows && inputs.build_pdf }}" == "true" ]]; then
ARTIFACT_NAME="${TARGET}-pdf-vol2-windows"
echo "windows_pdf_vol2_artifact=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
echo "📑 Windows PDF artifact (Vol II): $ARTIFACT_NAME"
fi
if [[ "${{ inputs.build_windows && inputs.build_epub }}" == "true" ]]; then
ARTIFACT_NAME="${TARGET}-epub-vol2-windows"
echo "windows_epub_vol2_artifact=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
echo "📚 Windows EPUB artifact (Vol II): $ARTIFACT_NAME"
fi
fi
echo "✅ API contract established - Status: $BUILD_SUCCESS_MSG"