Files
cs249r_book/.github/workflows/book-build-baremetal.yml
Vijay Janapa Reddi 96fa7ac5e5 chore: bump Quarto to 1.9.27 and R to 4.5.2
- Quarto 1.9.27: Linux (.deb), Windows (direct download; Scoop Extras has 1.8.27)
- R 4.5.2: Linux (CRAN jammy-cran40), Windows (Scoop main/r)
- Baremetal: quarto-actions/setup for both Linux and Windows
- Remove ggrepel version pin (R 4.5.x supports ggrepel 0.9.7)
- Update docs: BUILD.md, CONTAINER_BUILDS.md, docker READMEs
2026-03-02 17:36:35 -05:00

1469 lines
60 KiB
YAML

name: '📚 Book · 🔨 Build (Baremetal)'
# Concurrency disabled - allow unlimited parallel builds
# This workflow builds a Quarto project and uploads artifacts
# It handles both Windows and Linux environments with extensive caching for better performance
# Note: This workflow does NOT deploy - all deployment is handled by publish-live workflow
on:
workflow_dispatch:
inputs:
# Platform selection checkboxes
build_linux:
description: '🐧 Build on Linux'
required: false
default: true
type: boolean
build_windows:
description: '🪟 Build on Windows'
required: false
default: true
type: boolean
# Format selection checkboxes
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 configuration
build_target:
description: '📦 Build target (vol1, vol2)'
required: false
type: choice
default: 'vol1'
options:
- vol1
- vol2
target:
description: 'Target branch (dev/main)'
required: false
type: choice
default: 'dev'
options:
- dev
- main
# Advanced options
quarto-version:
description: 'Version of Quarto to use'
required: false
type: string
default: '1.9.27'
r-version:
description: 'Version of R to use'
required: false
type: string
default: '4.5.2'
quarto-log-level:
description: 'Quarto log level'
required: false
type: choice
default: 'INFO'
options:
- INFO
- DEBUG
artifact_name:
description: 'Custom artifact name (optional)'
required: false
type: string
default: ''
workflow_call:
inputs:
# Platform selection
build_linux:
required: false
type: boolean
default: false
description: 'Build on Linux'
build_windows:
required: false
type: boolean
default: false
description: 'Build on Windows'
# Format selection
build_html:
required: false
type: boolean
default: false
description: 'Build HTML format'
build_pdf:
required: false
type: boolean
default: false
description: 'Build PDF format'
build_epub:
required: false
type: boolean
default: false
description: 'Build EPUB format'
# Build configuration
build_target:
required: false
type: string
default: 'vol1'
description: 'Build target (vol1/vol2)'
target:
required: false
type: string
default: 'dev'
description: 'Target branch (dev/main)'
# Configuration options
quarto-version:
required: false
type: string
default: '1.9.27'
description: 'Version of Quarto to use'
r-version:
required: false
type: string
default: '4.5.2'
description: 'Version of R to use'
artifact_name:
required: false
type: string
default: ''
description: 'Custom artifact name (optional)'
outputs:
build_success:
description: "Whether all builds completed successfully"
value: ${{ jobs.collect-outputs.outputs.build_success }}
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 }}
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 }}
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_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: write
pages: write
# =============================================================================
# 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.os_emoji }} Build ${{ matrix.os_name }} (${{ matrix.format_emoji }} ${{ matrix.format }})'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# Linux Volume I builds
- os: ubuntu-latest
os_name: Linux
os_emoji: '🐧'
format: HTML
format_emoji: '📄'
volume: vol1
config: _quarto-html-vol1.yml
render_target: html
output_dir: _build/html-vol1
enabled: ${{ inputs.build_linux && inputs.build_html }}
- os: ubuntu-latest
os_name: Linux
os_emoji: '🐧'
format: PDF
format_emoji: '📑'
volume: vol1
config: _quarto-pdf-vol1.yml
render_target: titlepage-pdf
output_dir: _build/pdf-vol1
enabled: ${{ inputs.build_linux && inputs.build_pdf }}
- os: ubuntu-latest
os_name: Linux
os_emoji: '🐧'
format: EPUB
format_emoji: '📚'
volume: vol1
config: _quarto-epub-vol1.yml
render_target: epub
output_dir: _build/epub-vol1
enabled: ${{ inputs.build_linux && inputs.build_epub }}
# Linux Volume II builds
- os: ubuntu-latest
os_name: Linux
os_emoji: '🐧'
format: HTML
format_emoji: '📄'
volume: vol2
config: _quarto-html-vol2.yml
render_target: html
output_dir: _build/html-vol2
enabled: ${{ inputs.build_linux && inputs.build_html }}
- os: ubuntu-latest
os_name: Linux
os_emoji: '🐧'
format: PDF
format_emoji: '📑'
volume: vol2
config: _quarto-pdf-vol2.yml
render_target: titlepage-pdf
output_dir: _build/pdf-vol2
enabled: ${{ inputs.build_linux && inputs.build_pdf }}
- os: ubuntu-latest
os_name: Linux
os_emoji: '🐧'
format: EPUB
format_emoji: '📚'
volume: vol2
config: _quarto-epub-vol2.yml
render_target: epub
output_dir: _build/epub-vol2
enabled: ${{ inputs.build_linux && inputs.build_epub }}
# Windows Volume I builds
- os: windows-latest
os_name: Windows
os_emoji: '🪟'
format: HTML
format_emoji: '📄'
volume: vol1
config: _quarto-html-vol1.yml
render_target: html
output_dir: _build/html-vol1
enabled: ${{ inputs.build_windows && inputs.build_html }}
- os: windows-latest
os_name: Windows
os_emoji: '🪟'
format: PDF
format_emoji: '📑'
volume: vol1
config: _quarto-pdf-vol1.yml
render_target: titlepage-pdf
output_dir: _build/pdf-vol1
enabled: ${{ inputs.build_windows && inputs.build_pdf }}
- os: windows-latest
os_name: Windows
os_emoji: '🪟'
format: EPUB
format_emoji: '📚'
volume: vol1
config: _quarto-epub-vol1.yml
render_target: epub
output_dir: _build/epub-vol1
enabled: ${{ inputs.build_windows && inputs.build_epub }}
# Windows Volume II builds
- os: windows-latest
os_name: Windows
os_emoji: '🪟'
format: HTML
format_emoji: '📄'
volume: vol2
config: _quarto-html-vol2.yml
render_target: html
output_dir: _build/html-vol2
enabled: ${{ inputs.build_windows && inputs.build_html }}
- os: windows-latest
os_name: Windows
os_emoji: '🪟'
format: PDF
format_emoji: '📑'
volume: vol2
config: _quarto-pdf-vol2.yml
render_target: titlepage-pdf
output_dir: _build/pdf-vol2
enabled: ${{ inputs.build_windows && inputs.build_pdf }}
- os: windows-latest
os_name: Windows
os_emoji: '🪟'
format: EPUB
format_emoji: '📚'
volume: vol2
config: _quarto-epub-vol2.yml
render_target: epub
output_dir: _build/epub-vol2
enabled: ${{ inputs.build_windows && inputs.build_epub }}
timeout-minutes: 120 # ⏰ Set job timeout to 2 hours (7200 seconds)
outputs:
os_name: ${{ matrix.os_name }}
format: ${{ matrix.format }}
artifact_name: ${{ steps.build-declaration.outputs.artifact_name }}
status: ${{ job.status }}
output_dir: ${{ steps.build.outputs.output_dir }}
env:
R_LIBS_USER: ${{ github.workspace }}/.r-lib
QUARTO_LOG_LEVEL: ${{ inputs.quarto-log-level || 'INFO' }}
# UTF-8 encoding for proper emoji display
PYTHONIOENCODING: utf-8
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
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:
fetch-depth: 0
# === WINDOWS: Install Scoop and Chocolatey (Package Managers) ===
- name: 📦 Install Scoop Package Manager (Windows)
if: matrix.enabled && runner.os == 'Windows'
shell: pwsh
run: |
Write-Output "=== INSTALLING SCOOP PACKAGE MANAGER ==="
Write-Output "This matches the Windows container setup (Phase 5)"
if (-not (Get-Command scoop -ErrorAction SilentlyContinue)) {
Write-Output "🔤 Setting UTF-8 encoding..."
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
Write-Output "✅ UTF-8 encoding set"
Write-Output "🔐 Setting execution policy..."
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -Force
Write-Output "✅ Execution policy set"
Write-Output "📦 Installing Scoop package manager..."
Write-Output "📥 Downloading Scoop install script..."
Invoke-WebRequest -useb get.scoop.sh -outfile 'install-scoop.ps1'
Write-Output "📥 Scoop install script downloaded"
Write-Output "📦 Running Scoop installer..."
& .\install-scoop.ps1 -RunAsAdmin
Write-Output "✅ Scoop installed"
} else {
Write-Output "✅ Scoop already installed"
}
# Add Scoop to PATH for subsequent steps
$scoopShims = Join-Path (Resolve-Path ~).Path 'scoop\shims'
Write-Output "📁 Scoop shims path: $scoopShims"
Write-Output "🔗 Adding Scoop shims to PATH..."
echo "$scoopShims" | Out-File -Append -Encoding UTF8 $env:GITHUB_PATH
# Also add to current session PATH
$env:PATH = "$scoopShims;$env:PATH"
# Verify scoop is now available
Write-Output "🔍 Verifying Scoop installation..."
scoop --version
Write-Output "✅ Scoop verified in current session"
# Add Git (required for buckets)
Write-Output "📦 Installing Git..."
scoop install git
Write-Output "✅ Git installed"
# Add r-bucket for R packages
Write-Output "📦 Adding r-bucket..."
scoop bucket add r-bucket https://github.com/cderv/r-bucket.git
Write-Output "✅ r-bucket added"
# Add extras bucket
Write-Output "📦 Adding extras bucket..."
scoop bucket add extras
Write-Output "✅ extras bucket added"
Write-Output "✅ Scoop setup complete"
- name: 📦 Install Chocolatey Package Manager (Windows)
if: matrix.enabled && runner.os == 'Windows'
shell: pwsh
run: |
Write-Output "=== INSTALLING CHOCOLATEY PACKAGE MANAGER ==="
Write-Output "This matches the Windows container setup (Phase 2)"
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
Write-Output "📦 Installing Chocolatey..."
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
iex ((New-Object Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
Write-Output "✅ Chocolatey installed"
# Refresh environment variables for current session
$env:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.."
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
refreshenv
} else {
Write-Output "✅ Chocolatey already installed"
}
choco --version
Write-Output "✅ Chocolatey setup complete"
# === WINDOWS: Install Tools via Scoop (same as container) ===
- name: 🐍 Install Python via Scoop (Windows)
if: matrix.enabled && runner.os == 'Windows'
shell: pwsh
run: |
Write-Output "=== INSTALLING PYTHON VIA SCOOP ==="
Write-Output "This matches the Windows container setup (Phase 9)"
Write-Output "Installing Python 3.13 (3.14 not yet supported by pydantic-core)"
# Add versions bucket for specific version installs
Write-Output "📦 Adding versions bucket..."
scoop bucket add versions
Write-Output "✅ Versions bucket added"
# List available Python versions
Write-Output "📋 Available Python packages:"
scoop search python | Select-String "python"
# Install Python 3.13 from versions bucket
Write-Output "📦 Installing Python 3.13 from versions bucket..."
scoop install versions/python313
if ($LASTEXITCODE -ne 0) {
Write-Error "❌ Failed to install versions/python313"
Write-Output "⚠️ Trying alternative: python3.13 or python from main..."
# Try python3.13 as alternative name
scoop install python3.13 -ErrorAction SilentlyContinue
if ($LASTEXITCODE -ne 0) {
Write-Output "⚠️ python3.13 not found, trying main/python..."
scoop install main/python
}
}
Write-Output "✅ Python install command completed"
# Check which Python app was installed
Write-Output "🔍 Checking installed Python apps..."
scoop list | Select-String "python"
# Try to get Python path - try different app names
$pythonPath = $null
$appNames = @('python313', 'python3.13', 'python')
foreach ($appName in $appNames) {
$pythonPath = scoop prefix $appName 2>$null
if ($pythonPath) {
Write-Output "✅ Found Python as '$appName'"
break
}
}
if (-not $pythonPath) {
Write-Error "❌ Could not find Python installation via Scoop"
Write-Error "Checked app names: python313, python3.13, python"
scoop list
exit 1
}
Write-Output "📁 Python location: $pythonPath"
# Add to GITHUB_PATH for subsequent steps
Write-Output "🔗 Adding Python to GITHUB_PATH for future steps..."
echo "$pythonPath" | Out-File -Append -Encoding UTF8 $env:GITHUB_PATH
echo "$pythonPath\Scripts" | Out-File -Append -Encoding UTF8 $env:GITHUB_PATH
Write-Output "✅ Python added to GITHUB_PATH"
# Also add to current session
$env:PATH = "$pythonPath;$pythonPath\Scripts;$env:PATH"
$pythonVersion = python --version
Write-Output "📊 Python version: $pythonVersion"
# Verify we got Python 3.13
if ($pythonVersion -notmatch "3\.13") {
Write-Error "❌ Expected Python 3.13, got: $pythonVersion"
exit 1
}
Write-Output "✅ Python 3.13 installation verified and added to PATH"
- name: 📊 Install R via Scoop (Windows)
if: matrix.enabled && runner.os == 'Windows'
shell: pwsh
run: |
Write-Output "=== INSTALLING R VIA SCOOP ==="
Write-Output "This matches the Windows container setup (Phase 12)"
Write-Output "📦 Installing R ${{ inputs.r-version }} from main bucket..."
scoop install main/r@${{ inputs.r-version }}
Write-Output "✅ R installed"
Write-Output "📊 Verifying R installation..."
R.exe --version
Write-Output "✅ R installation complete"
- name: 🎨 Install Inkscape via Scoop (Windows)
if: matrix.enabled && runner.os == 'Windows'
shell: pwsh
run: |
Write-Output "=== INSTALLING INKSCAPE VIA SCOOP ==="
Write-Output "This matches the Windows container setup (Phase 8)"
Write-Output "📦 Installing Inkscape..."
scoop install inkscape
Write-Output "✅ Inkscape installed"
Write-Output "📊 Inkscape version:"
inkscape --version
Write-Output "✅ Inkscape installation complete"
- name: 📦 Install Ghostscript via Scoop (Windows)
if: matrix.enabled && runner.os == 'Windows'
shell: pwsh
run: |
Write-Output "=== INSTALLING GHOSTSCRIPT VIA SCOOP ==="
Write-Output "This matches the Windows container setup (Phase 7)"
Write-Output "📦 Installing Ghostscript..."
scoop install main/ghostscript
Write-Output "✅ Ghostscript installed"
Write-Output "📊 Ghostscript version:"
gs --version
Write-Output "✅ Ghostscript installation complete"
- name: 📦 Install Visual C++ Redistributable (Windows)
if: matrix.enabled && runner.os == 'Windows'
shell: pwsh
run: |
Write-Output "=== INSTALLING VISUAL C++ REDISTRIBUTABLE ==="
Write-Output "This matches the Windows container setup (Phase 11)"
Write-Output "Required for Quarto DLL dependencies on Windows"
choco install vcredist-all -y
Write-Output "✅ Visual C++ Redistributable installed"
# === LINUX: Setup Python (GitHub Action) ===
- name: 🐍 Set up Python (Linux)
if: matrix.enabled && runner.os == 'Linux'
uses: actions/setup-python@v6
with:
python-version: '3.13'
# === Setup Quarto (Linux + Windows via quarto-actions) ===
- name: 📦 Setup Quarto
if: matrix.enabled
uses: quarto-dev/quarto-actions/setup@v2
with:
version: ${{ inputs.quarto-version }}
# === CROSS-PLATFORM: Verification ===
- name: 📋 Quarto Setup Info
if: matrix.enabled
shell: bash
run: |
echo "🔄 Checking Quarto installation..."
quarto check
echo "📊 Quarto version info:"
quarto --version
echo "📍 Quarto installation location:"
which quarto || where.exe quarto
- name: 💾 Cache Python packages
if: matrix.enabled
uses: actions/cache@v5
id: cache-python-packages
with:
path: |
~/.cache/pip
~\AppData\Local\pip\Cache
key: python-pkgs-${{ runner.os }}-${{ hashFiles(format('{0}/requirements.txt', vars.BOOK_DEPS)) }}
restore-keys: |
python-pkgs-${{ runner.os }}-
# Install Ghostscript before Python package verification
- name: 📦 Install Ghostscript (Linux)
if: matrix.enabled && runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y ghostscript
- name: 📦 Install Python dependencies (Windows)
if: matrix.enabled && runner.os == 'Windows'
shell: pwsh
run: |
Write-Output "=== INSTALLING PYTHON DEPENDENCIES ==="
Write-Output "Using Scoop-installed Python (should be 3.13.x)"
# Try to find Scoop's Python with different app names
$scoopPython = $null
$appNames = @('python313', 'python3.13', 'python')
foreach ($appName in $appNames) {
$path = scoop prefix $appName 2>$null
if ($path) {
$scoopPython = $path
Write-Output "✅ Found Scoop Python as '$appName'"
break
}
}
if (-not $scoopPython) {
Write-Error "❌ Could not find Scoop Python installation"
Write-Error "GITHUB_PATH was not set correctly in previous step"
Write-Error "Current Python being used:"
python --version
(Get-Command python).Source
exit 1
}
Write-Output "📁 Scoop Python location: $scoopPython"
# Prepend to PATH and verify immediately
$env:PATH = "$scoopPython;$scoopPython\Scripts;$env:PATH"
# Verify we're using the right Python
$pythonExe = (Get-Command python).Source
Write-Output "📍 Python executable being used: $pythonExe"
$pythonVersion = python --version
Write-Output "📊 Python version: $pythonVersion"
# Fail if not Python 3.13
if ($pythonVersion -notmatch "3\.13") {
Write-Error "❌ Wrong Python. Expected 3.13 from Scoop, got: $pythonVersion"
Write-Error "Python location: $pythonExe"
Write-Error "This means PATH is not configured correctly"
exit 1
}
Write-Output "✅ Confirmed using Scoop Python 3.13"
Write-Output "📦 Upgrading pip..."
python -m pip install --upgrade pip
Write-Output "📦 Installing Python packages from requirements.txt..."
python -m pip install -r ${{ vars.BOOK_DEPS }}/requirements.txt
Write-Output "✅ Python dependencies installed"
- name: 📦 Install Python dependencies (Linux)
if: matrix.enabled && runner.os == 'Linux'
run: |
python -m pip install --upgrade pip
python -m pip install -r ${{ vars.BOOK_DEPS }}/requirements.txt
# Cache Linux system packages without hardcoded paths
- name: 💾 Cache APT packages
if: matrix.enabled && runner.os == 'Linux'
uses: actions/cache@v5
id: cache-apt
with:
path: ~/.apt-cache
key: apt-${{ runner.os }}-${{ hashFiles('.github/workflows/*.yml') }}
restore-keys: |
apt-${{ runner.os }}-
- name: 🛠️ Install Linux Dependencies
if: matrix.enabled && runner.os == 'Linux' && steps.cache-apt.outputs.cache-hit != 'true'
shell: bash
run: |
echo "🔄 Installing Linux dependencies..."
echo "📦 Creating APT cache directory"
mkdir -p ~/.apt-cache
echo "📦 Updating package lists"
sudo apt-get update
echo "📦 Installing required system libraries"
sudo apt-get -o dir::cache::archives="$HOME/.apt-cache" install -y \
fonts-dejavu \
fonts-freefont-ttf \
gdk-pixbuf2.0-bin \
libcairo2 \
libfontconfig1 \
libfontconfig1-dev \
libfreetype6 \
libfreetype6-dev \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libpangoft2-1.0-0 \
libxml2-dev \
libcurl4-openssl-dev \
libjpeg-dev \
libtiff5-dev \
libpng-dev \
libharfbuzz-dev \
libfribidi-dev \
librsvg2-dev \
libgdal-dev \
libudunits2-dev
echo "✅ Linux dependencies installed"
- name: 🎨 Install Inkscape and font dependencies (Linux)
if: matrix.enabled && runner.os == 'Linux'
run: |
# First remove any existing Inkscape
sudo apt-get remove -y inkscape || true
# Install Inkscape from PPA for more reliable version
echo "📦 Installing Inkscape from PPA..."
sudo add-apt-repository ppa:inkscape.dev/stable -y
sudo apt-get update
sudo apt-get install -y inkscape
# Install font dependencies
echo "📦 Installing font dependencies..."
sudo apt-get install -y \
fonts-freefont-ttf \
fonts-liberation \
fontconfig
# Update font cache after installing Inkscape and fonts
echo "🧹 Updating font cache..."
sudo fc-cache -fv
# Verify Inkscape installation
echo "📊 Inkscape version:"
inkscape --version
# Test SVG to PDF conversion with the new Inkscape
echo "🧪 Testing Inkscape SVG to PDF conversion..."
echo '<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><circle cx="50" cy="50" r="40" fill="red"/></svg>' > test.svg
inkscape --export-type=pdf --export-filename=test.pdf test.svg
# Verify if the PDF was created
if [ -f test.pdf ]; then
echo "✅ Inkscape SVG to PDF conversion successful!"
ls -lh test.pdf
else
echo "❌ Inkscape SVG to PDF conversion failed."
echo "🔍 Checking Inkscape installation..."
dpkg -l | grep inkscape
which inkscape
ldd $(which inkscape) | grep "not found" || echo "All dependencies resolved"
fi
# Install TeX Live packages - ALWAYS install for consistency
# Required for TikZ diagrams in HTML (PDF->SVG), PDF output, and potential future needs
# === WINDOWS: Install TeX Live via Chocolatey (same as container) ===
- name: 📦 Install TeX Live via Chocolatey (Windows)
if: matrix.enabled && runner.os == 'Windows'
shell: pwsh
run: |
Write-Output "=== INSTALLING TEX LIVE VIA CHOCOLATEY ==="
Write-Output "This matches the Windows container setup (Phase 4)"
Write-Output "📦 Installing TeX Live via Chocolatey (pinned to 2025.20251008.0)..."
choco install texlive --version=2025.20251008.0 -y
if ($LASTEXITCODE -ne 0) {
Write-Error "❌ TeX Live installation failed"
exit 1
}
Write-Output "✅ TeX Live installed via Chocolatey"
Write-Output "🔍 Finding TeX Live installation directory..."
$texRoot = Join-Path $env:SystemDrive 'texlive'
Write-Output "📁 TeX Live root: $texRoot"
Write-Output "🔍 Looking for year-based directories..."
$texYearDir = Get-ChildItem $texRoot -Directory |
Where-Object { $_.Name -match '^\d{4}$' } |
Sort-Object Name -Descending |
Select-Object -First 1
Write-Output "📁 Found year directory: $($texYearDir.FullName)"
$texLiveBin = Join-Path $texYearDir.FullName 'bin\windows'
Write-Output "📁 TeX Live bin directory: $texLiveBin"
Write-Output "🔧 Adding TeX Live to PATH..."
echo "$texLiveBin" | Out-File -Append -Encoding UTF8 $env:GITHUB_PATH
$env:PATH = "$texLiveBin;$env:PATH"
Write-Output "✅ PATH updated with: $texLiveBin"
Write-Output "📋 Reading collections from tl_packages..."
if (Test-Path '${{ vars.BOOK_DEPS }}/tl_packages') {
$collections = Get-Content '${{ vars.BOOK_DEPS }}/tl_packages' |
Where-Object { $_.Trim() -ne '' -and -not $_.Trim().StartsWith('#') }
Write-Output "📦 Found $($collections.Count) collections to install"
Write-Output "📋 Collections:"
$collections | ForEach-Object { Write-Output " - $_" }
Write-Output "🔄 Installing collections..."
$i = 1
foreach ($collection in $collections) {
Write-Output "📦 [$i/$($collections.Count)] Installing $collection..."
& "$texLiveBin\tlmgr.bat" install $collection
if ($LASTEXITCODE -eq 0) {
Write-Output "✅ $collection installed successfully"
} else {
Write-Output "⚠️ Failed to install $collection, continuing..."
}
$i++
}
Write-Output "✅ Collection installation complete"
} else {
Write-Output "⚠️ No tl_packages file found, skipping collection installation"
}
Write-Output "🔄 Updating tlmgr..."
& "$texLiveBin\tlmgr.bat" update --self --all
Write-Output "✅ tlmgr updated"
Write-Output "🔍 Verifying lualatex installation..."
& "$texLiveBin\lualatex.exe" --version
Write-Output "✅ TeX Live installation verified"
# === LINUX: Install TeX Live via GitHub Action ===
- name: 📦 Install TeX Live packages (Linux)
if: matrix.enabled && runner.os == 'Linux'
uses: zauguin/install-texlive@v4
with:
package_file: ${{ vars.BOOK_DEPS }}/tl_packages
texlive_version: 2025
cache_version: 1
- name: 🔍 Verify TeX Live Installation
if: matrix.enabled
shell: bash
run: |
echo "🔄 Verifying TeX Live installation (installed for all builds)..."
echo "📊 Current format: ${{ matrix.format }}"
echo "📊 Philosophy: All builds get same environment, only build targets differ"
# Check LaTeX engines
echo "📊 Checking LaTeX engines:"
which lualatex || echo "❌ lualatex not found"
which pdflatex || echo "❌ pdflatex not found"
lualatex --version | head -2 || echo "❌ lualatex version failed"
# Check if required packages are available
echo "📊 Checking core LaTeX and TikZ packages:"
kpsewhich pgf.sty && echo "✅ PGF package found" || echo "❌ PGF package missing"
kpsewhich pgfplots.sty && echo "✅ PGFPlots package found" || echo "❌ PGFPlots package missing"
kpsewhich xcolor.sty && echo "✅ XColor package found" || echo "❌ XColor package missing"
kpsewhich amsmath.sty && echo "✅ AMSMath package found" || echo "❌ AMSMath package missing"
kpsewhich standalone.cls && echo "✅ Standalone class found" || echo "❌ Standalone class missing"
echo "📊 Checking font packages:"
kpsewhich phvr7t.tfm && echo "✅ Helvetica font found" || echo "❌ Helvetica font missing"
kpsewhich t1phv.fd && echo "✅ Helvetica font descriptor found" || echo "❌ Helvetica font descriptor missing"
# Test TikZ compilation
echo "🧪 Testing TikZ compilation..."
cat > test_tikz.tex << 'EOF'
\documentclass{standalone}
\usepackage{tikz}
\usepackage{pgfplots}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usetikzlibrary{positioning}
\usetikzlibrary{calc}
\begin{document}
\begin{tikzpicture}[font=\small\usefont{T1}{phv}{m}{n}]
\node[draw, fill=blue!20] at (0,0) {TikZ Test};
\node[draw, fill=red!20] at (2,0) {Success};
\draw[->] (0.8,0) -- (1.2,0);
\end{tikzpicture}
\end{document}
EOF
if lualatex -interaction=nonstopmode test_tikz.tex; then
echo "✅ TikZ compilation successful"
ls -la test_tikz.pdf
else
echo "❌ TikZ compilation failed"
cat test_tikz.log | tail -20 || echo "No log file found"
fi
rm -f test_tikz.*
# === LINUX: Setup R via GitHub Action ===
- name: 📊 Setup R (Linux)
if: matrix.enabled && runner.os == 'Linux'
uses: r-lib/actions/setup-r@v2
with:
r-version: ${{ inputs.r-version }}
use-public-rspm: true
# === CROSS-PLATFORM: R Setup Info ===
- name: 📋 R Setup Info (Windows)
if: matrix.enabled && runner.os == 'Windows'
shell: pwsh
run: |
Write-Output "🔄 R Version Information:"
R.exe --version
Write-Output "📁 R home:"
Rscript -e 'cat(R.home(), "\n")'
Write-Output "📊 R library paths:"
Rscript -e '.libPaths()'
- name: 📋 R Setup Info (Linux)
if: matrix.enabled && runner.os == 'Linux'
shell: bash
run: |
echo "🔄 R Version Information:"
R --version | head -3
echo "📁 R home:"
Rscript -e 'cat(R.home(), "\n")'
echo "📊 R library paths:"
Rscript -e '.libPaths()'
# Cache R packages using standard paths
# Cache version bumped to force reinstall when adding packages (e.g. ggrepel for hw_acceleration.qmd)
- name: 💾 Cache R packages
if: matrix.enabled
uses: actions/cache@v5
id: cache-r-packages
with:
path: |
${{ env.R_LIBS_USER }}
key: r-pkgs-${{ runner.os }}-${{ inputs.r-version }}-v2-${{ hashFiles(format('{0}/install_packages.R', vars.BOOK_DEPS), '**/*.qmd') }}
restore-keys: |
r-pkgs-${{ runner.os }}-${{ inputs.r-version }}-v2-
- name: 📦 Install R packages
if: matrix.enabled && steps.cache-r-packages.outputs.cache-hit != 'true'
shell: Rscript {0}
run: |
# This matches the Windows container setup (Phase 13)
# Set options for better package installation
options(repos = c(CRAN = "https://cran.rstudio.com"))
cat("=== INSTALLING R PACKAGES ===\n")
cat("This matches the Windows container setup\n\n")
cat("🔄 Setting up R environment...\n")
cat(paste("R library path:", Sys.getenv("R_LIBS_USER"), "\n"))
# Create and set library path
lib_path <- Sys.getenv("R_LIBS_USER")
dir.create(lib_path, showWarnings = FALSE, recursive = TRUE)
.libPaths(lib_path)
# Install remotes first
cat("📦 Installing remotes package...\n")
install.packages("remotes")
# Install packages from install_packages.R
if (file.exists("${{ vars.BOOK_DEPS }}/install_packages.R")) {
cat("📦 Installing packages from ${{ vars.BOOK_DEPS }}/install_packages.R...\n")
source("${{ vars.BOOK_DEPS }}/install_packages.R")
} else {
cat("⚠️ No ${{ vars.BOOK_DEPS }}/install_packages.R found, installing common packages\n")
pkgs <- c("rmarkdown", "knitr", "tidyverse", "ggplot2", "bookdown")
cat(paste("📦 Installing packages:", paste(pkgs, collapse=", "), "\n"))
install.packages(pkgs)
}
# Verify critical packages (same as container does inline)
cat("\n🔍 Verifying R package installation...\n")
for (p in c("rmarkdown", "knitr", "ggrepel")) {
if (!require(p, character.only=TRUE, quietly=TRUE)) {
stop(paste("missing:", p))
}
}
cat("✅ R package installation complete\n")
cat("📊 Installed packages:\n")
ip <- installed.packages()[, "Package"]
print(head(ip, 10))
cat(paste("Total packages installed:", length(ip), "\n"))
# === WINDOWS: Comprehensive Verification (matches container FINAL CHECKS) ===
- name: 🔍 Comprehensive Windows Environment Verification
if: matrix.enabled && runner.os == 'Windows'
shell: pwsh
run: |
Write-Output "=== FINAL VERIFICATION WITH ENHANCED DIAGNOSTICS ==="
Write-Output "This matches the Windows container FINAL CHECKS phase"
Write-Output ""
Write-Output "🔍 SYSTEM DIAGNOSTICS:"
Write-Output "----------------------"
Write-Output "PATH environment variable:"
Write-Output $env:PATH
Write-Output ""
Write-Output "Visual C++ Redistributable check:"
Get-ChildItem 'C:\Windows\System32' -Filter 'msvcp*.dll' -ErrorAction SilentlyContinue | Select-Object Name, Length, LastWriteTime
Write-Output ""
Write-Output "📊 TOOL VERIFICATION:"
Write-Output "---------------------"
Write-Output "Checking Quarto..."
try {
quarto --version
Write-Output "✅ Quarto version check: PASSED"
Write-Output "Running Quarto check for comprehensive validation..."
& quarto check 2>&1 | Write-Output
if ($LASTEXITCODE -eq 0) {
Write-Output "✅ Quarto check: PASSED"
} else {
Write-Output "⚠️ Quarto check: ISSUES DETECTED"
Write-Output "Exit code: $LASTEXITCODE"
}
} catch {
Write-Output "❌ Quarto verification failed:"
Write-Output $_.Exception.Message
}
Write-Output "Checking Python..."
python --version
Write-Output "✅ Python verified"
Write-Output "Checking R..."
R.exe --version
Write-Output "✅ R verified"
Write-Output "Checking LaTeX..."
lualatex --version
Write-Output "✅ LaTeX verified"
Write-Output "Checking Ghostscript..."
gs --version
Write-Output "✅ Ghostscript verified"
Write-Output "Checking Inkscape..."
inkscape --version
Write-Output "✅ Inkscape verified"
Write-Output ""
Write-Output "🎯 VERIFICATION STATUS:"
Write-Output "------------------------"
Write-Output "✅ All tools verified successfully"
Write-Output "Ready to proceed with build"
- name: 🔨 Build ${{ matrix.format }}
if: matrix.enabled
id: build
shell: bash
run: |
OUTPUT_DIR="${{ matrix.output_dir }}"
echo "output_dir=${OUTPUT_DIR}" >> "$GITHUB_OUTPUT"
echo "🚀 Setting up ${{ matrix.format }} configuration..."
echo "🔍 DEBUG: Current directory: $(pwd)"
echo "🔍 DEBUG: Listing repository root:"
ls -la
echo "🔍 DEBUG: Checking ${{ vars.BOOK_QUARTO }} exists:"
ls -la ${{ vars.BOOK_QUARTO }}/ || echo "${{ vars.BOOK_QUARTO }} not found!"
cd ${{ vars.BOOK_QUARTO }}
echo "🔍 DEBUG: Now in $(pwd)"
echo "🔍 DEBUG: Listing current directory:"
ls -la
echo "🔍 DEBUG: Checking config directory:"
ls -la config/ || echo "config directory not found!"
rm -f _quarto.yml
cp config/${{ matrix.config }} _quarto.yml
echo "✅ Configuration set to ${{ matrix.format }}"
echo "🔨 Building ${{ matrix.format }}..."
# Update status to building (with timeout, failures are non-fatal)
curl --max-time 10 --retry 2 --retry-delay 2 -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }}" \
-d "{
\"state\": \"pending\",
\"description\": \"Building ${{ matrix.format }} content (${{ matrix.os_name }})\",
\"context\": \"ci/quarto-build-${{ matrix.os_name }}-${{ matrix.format }}\"
}" || echo "⚠️ Failed to update commit status to pending (non-fatal)"
quarto render --to ${{ matrix.render_target }} --output-dir "../${OUTPUT_DIR}"
echo "✅ ${{ matrix.format }} build completed"
- name: 📋 Check Quarto Build Output
if: matrix.enabled
shell: bash
run: |
echo "🔄 Checking Quarto build output for ${{ matrix.format }}..."
echo "🔍 DEBUG: Current directory: $(pwd)"
cd ${{ vars.BOOK_ROOT }}
echo "🔍 DEBUG: Changed to: $(pwd)"
if [ -d "${{ steps.build.outputs.output_dir }}" ]; then
echo "✅ ${{ steps.build.outputs.output_dir }} directory exists"
echo "📊 Files in ${{ steps.build.outputs.output_dir }} directory:"
ls -la ${{ steps.build.outputs.output_dir }} | head -n 20
echo "📊 Total files in ${{ steps.build.outputs.output_dir }}:"
find ${{ steps.build.outputs.output_dir }} -type f | wc -l
else
echo "❌ ${{ steps.build.outputs.output_dir }} directory not found!"
exit 1
fi
- name: 📉 Compress PDF (Linux)
if: matrix.enabled && matrix.format == 'PDF' && runner.os == 'Linux'
working-directory: ${{ vars.BOOK_ROOT }}/${{ steps.build.outputs.output_dir }}
run: |
if [ -f "Machine-Learning-Systems.pdf" ]; then
echo "📉 Compressing PDF with professional compression tool..."
python3 ${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/compress_pdf.py \
--input "Machine-Learning-Systems.pdf" \
--output "compressed.pdf" \
--quality minimal \
--verbose
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.enabled && matrix.format == 'PDF' && runner.os == 'Windows'
shell: pwsh
working-directory: ${{ vars.BOOK_ROOT }}/${{ steps.build.outputs.output_dir }}
run: |
# Set UTF-8 encoding for proper emoji display
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$input = "Machine-Learning-Systems.pdf"
$output = "compressed.pdf"
if (!(Test-Path $input)) {
Write-Warning "⚠️ Input PDF not found! Skipping compression..."
exit 0 # Non-zero exit would fail the workflow
}
Write-Output "📉 Compressing PDF with professional compression tool..."
python ${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/compress_pdf.py --input $input --output $output --quality minimal --verbose
if (Test-Path $output) {
Write-Output "✅ PDF compression completed"
Move-Item -Force $output $input
} else {
Write-Warning "⚠️ Compression failed but continuing"
}
- name: 📚 Compress EPUB (Linux)
if: matrix.enabled && matrix.format == 'EPUB' && runner.os == 'Linux'
working-directory: ${{ vars.BOOK_ROOT }}/${{ steps.build.outputs.output_dir }}
run: |
if [ -f "Machine-Learning-Systems.epub" ]; then
echo "📚 Compressing EPUB with optimized compression tool..."
echo "🔍 DEBUG: GITHUB_WORKSPACE=${{ github.workspace }}"
echo "🔍 DEBUG: PWD=$(pwd)"
echo "🔍 DEBUG: Script path: ${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/compress_epub.py"
echo "🔍 DEBUG: Verifying Python and Pillow installation:"
python3 --version
python3 -c "import sys; print('Python path:', sys.executable)"
if ! python3 -c "import PIL; print('✅ Pillow version: ' + PIL.__version__)" 2>/dev/null; then
echo "⚠️ Pillow not found or incompatible, installing Pillow>=10.0.0..."
python3 -m pip install 'Pillow>=10.0.0' --quiet
python3 -c "import PIL; print('✅ Pillow version (installed): ' + PIL.__version__)"
fi
echo "🔍 DEBUG: Checking quarto directory structure:"
ls -la "${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/" || echo "❌ ${{ vars.BOOK_QUARTO }}/ not found"
echo "🔍 DEBUG: Checking for publish directory:"
ls -la "${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/" || echo "❌ ${{ vars.BOOK_QUARTO }}/publish/ not found"
echo "🔍 DEBUG: Repository root contents:"
ls -la "${{ github.workspace }}/" | head -10
python3 ${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/compress_epub.py \
--input "Machine-Learning-Systems.epub" \
--output "compressed.epub" \
--verbose
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.enabled && matrix.format == 'EPUB' && runner.os == 'Windows'
shell: pwsh
working-directory: ${{ vars.BOOK_ROOT }}/${{ steps.build.outputs.output_dir }}
run: |
# Set UTF-8 encoding for proper emoji display
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$input = "Machine-Learning-Systems.epub"
$output = "compressed.epub"
if (!(Test-Path $input)) {
Write-Warning "⚠️ Input EPUB not found! Skipping compression..."
exit 0 # Non-zero exit would fail the workflow
}
Write-Output "📚 Compressing EPUB with optimized compression tool..."
Write-Output "🔍 DEBUG: Verifying Python and Pillow installation:"
python --version
python -c "import sys; print('Python path:', sys.executable)"
try {
python -c "import PIL; print('✅ Pillow version: ' + PIL.__version__)"
} catch {
Write-Output "⚠️ Pillow not found or incompatible, installing Pillow>=10.0.0..."
python -m pip install 'Pillow>=10.0.0' --quiet
python -c "import PIL; print('✅ Pillow version (installed): ' + PIL.__version__)"
}
python ${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/compress_epub.py --input $input --output $output --verbose
if (Test-Path $output) {
Write-Output "✅ EPUB compression completed (using optimized defaults: quality=50, max-size=1000px)"
Move-Item -Force $output $input
} else {
Write-Warning "⚠️ Compression failed but continuing"
}
- name: 📋 Build Declaration
if: matrix.enabled
id: build-declaration
shell: bash
run: |
if [ -n "${{ inputs.artifact_name }}" ]; then
ARTIFACT_NAME_RAW="${{ inputs.artifact_name }}-${{ matrix.os_name }}-${{ matrix.format }}-${{ matrix.volume }}"
else
FORMAT_LC=$(echo "${{ matrix.format }}" | tr '[:upper:]' '[:lower:]')
OS_LC=$(echo "${{ matrix.os_name }}" | tr '[:upper:]' '[:lower:]')
ARTIFACT_NAME_RAW="${{ inputs.target }}-${FORMAT_LC}-${{ matrix.volume }}-${OS_LC}"
fi
echo "📦 Build Declaration: Successfully created artifact '$ARTIFACT_NAME_RAW'"
echo "artifact_name=$ARTIFACT_NAME_RAW" >> $GITHUB_OUTPUT
- name: 📤 Upload build artifacts
if: matrix.enabled
uses: actions/upload-artifact@v6
with:
name: ${{ steps.build-declaration.outputs.artifact_name }}
path: ${{ steps.build.outputs.output_dir }}
- name: 📝 Collect Build Logs and System Info (Universal)
if: matrix.enabled && always() # Always run, even if previous steps failed
shell: pwsh
run: |
# Set UTF-8 encoding for proper emoji display
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
Write-Output "🔄 Collecting comprehensive build logs and system information..."
# Create logs directory structure
New-Item -Type Directory -Path "logs", "logs/system-info", "logs/build-output" -Force | Out-Null
# === COLLECT ALL BUILD LOGS ===
Write-Output "📄 Searching for build logs in all directories..."
# Find and copy all .log files from common locations
$logPaths = @("quarto", "build", "_book", ".", "_site")
foreach ($path in $logPaths) {
if (Test-Path $path) {
Write-Output "🔍 Searching in: $path"
Get-ChildItem -Path $path -Recurse -Include "*.log" -ErrorAction SilentlyContinue | ForEach-Object {
$relativePath = $_.FullName.Replace($PWD.Path, "").TrimStart("/\")
$sanitizedName = $relativePath -replace "[/\\:]", "_"
Write-Output "📄 Found log: $relativePath → logs/$sanitizedName"
Copy-Item $_.FullName "logs/$sanitizedName" -ErrorAction SilentlyContinue
}
}
}
# Look for any output files that might contain build info
Get-ChildItem -Recurse -Include "*.out", "*.aux", "*.fls", "*.fdb_latexmk" -ErrorAction SilentlyContinue | ForEach-Object {
$relativePath = $_.FullName.Replace($PWD.Path, "").TrimStart("/\")
$sanitizedName = $relativePath -replace "[/\\:]", "_"
Copy-Item $_.FullName "logs/build-output/$sanitizedName" -ErrorAction SilentlyContinue
}
# === SYSTEM ENVIRONMENT ===
Write-Output "🖥️ Collecting system environment..."
@"
=== Build Environment Information ===
Date: $(Get-Date)
GitHub Workflow: ${{ github.workflow }}
GitHub Run ID: ${{ github.run_id }}
Runner OS: ${{ runner.os }}
Build Format: ${{ matrix.format }}
PowerShell Version: $($PSVersionTable.PSVersion)
Working Directory: $PWD
"@ | Out-File "logs/system-info/environment.log" -Encoding UTF8
# === QUARTO INFORMATION ===
Write-Output "📚 Collecting Quarto information..."
@"
=== Quarto Information ===
"@ | Out-File "logs/system-info/quarto-info.log" -Encoding UTF8
if (Get-Command quarto -ErrorAction SilentlyContinue) {
& quarto --version 2>&1 | Out-File "logs/system-info/quarto-info.log" -Append -Encoding UTF8
"--- Quarto Check ---" | Out-File "logs/system-info/quarto-info.log" -Append -Encoding UTF8
& quarto check 2>&1 | Out-File "logs/system-info/quarto-info.log" -Append -Encoding UTF8
} else {
"Quarto not found" | Out-File "logs/system-info/quarto-info.log" -Append -Encoding UTF8
}
# === SUMMARY ===
Write-Output "📊 Build log collection summary:"
Write-Output "📁 Log files collected:"
Get-ChildItem -Path "logs" -Recurse -File | ForEach-Object {
$size = if ($_.Length -gt 1MB) { "{0:N2} MB" -f ($_.Length / 1MB) }
elseif ($_.Length -gt 1KB) { "{0:N2} KB" -f ($_.Length / 1KB) }
else { "$($_.Length) bytes" }
Write-Output " 📄 $($_.FullName.Replace($PWD.Path, '.')) ($size)"
}
- name: 📤 Upload Build Logs (Always)
if: matrix.enabled && always() # Upload logs even if build fails
uses: actions/upload-artifact@v6
with:
name: build-logs-${{ matrix.os_name }}-${{ matrix.format }}-${{ github.run_id }}
path: logs/
if-no-files-found: warn
- name: 📋 Build Summary
if: matrix.enabled && always()
shell: pwsh
run: |
# Set UTF-8 encoding for proper emoji display
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# TeX Live is now always installed for consistent environment across all builds
$texLiveStatus = "✅ Always installed (zauguin/install-texlive@v4)"
@"
## 📊 Build Status Summary for ${{ matrix.os_name }} ${{ matrix.format }}
**Status: ${{ job.status }}**
🎯 Target: ${{ inputs.target }}
📚 Quarto Version: ${{ inputs.quarto-version }}
🔬 R Version: ${{ inputs.r-version }}
🧩 Cache Status:
- TeX Live: $texLiveStatus
- R Packages: ${{ steps.cache-r-packages.outputs.cache-hit == 'true' && '✅ Hit' || '❌ Miss' }}
- Python Packages: ${{ steps.cache-python-packages.outputs.cache-hit == 'true' && '✅ Hit' || '❌ Miss' }}
📝 Debug Artifacts:
- Build logs available as artifact: build-logs-${{ matrix.os_name }}-${{ matrix.format }}-${{ github.run_id }}
⏰ Completed at: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
"@ | Add-Content $env:GITHUB_STEP_SUMMARY
- name: 🎯 Set Final Build Status
if: matrix.enabled && always()
continue-on-error: true # Don't fail the workflow if status update fails
shell: bash
run: |
echo "🎯 Setting final build status..."
# Determine the final status based on job status
if [ "${{ job.status }}" = "success" ]; then
STATE="success"
DESCRIPTION="Build completed successfully (${{ matrix.os_name }}, ${{ matrix.format }})"
else
STATE="failure"
DESCRIPTION="Build failed (${{ matrix.os_name }}, ${{ matrix.format }})"
fi
echo "📊 Final status: $STATE"
echo "📝 Description: $DESCRIPTION"
# Set the final commit status with timeout and retry logic
echo "🔄 Attempting to update commit status..."
# Try with shorter timeout to fail fast if network is unreachable
if curl --max-time 10 --retry 2 --retry-delay 2 -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }}" \
-d "{
\"state\": \"$STATE\",
\"description\": \"$DESCRIPTION\",
\"context\": \"ci/quarto-build-${{ matrix.os_name }}-${{ matrix.format }}\"
}"; then
echo "✅ Commit status updated successfully"
else
echo "⚠️ Failed to update commit status (network timeout or connection issue)"
echo "This is non-fatal and the build status is still recorded in the workflow"
fi
collect-outputs:
name: '📊 Collect Outputs'
needs: build
runs-on: ubuntu-latest
if: always()
outputs:
build_success: ${{ steps.collect.outputs.build_success }}
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 }}
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 }}
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_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 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
TARGET="${{ inputs.target }}"
if [[ "${{ inputs.build_linux && inputs.build_html }}" == "true" ]]; then
echo "linux_html_vol1_artifact=${TARGET}-html-vol1-linux" >> $GITHUB_OUTPUT
echo "linux_html_vol2_artifact=${TARGET}-html-vol2-linux" >> $GITHUB_OUTPUT
fi
if [[ "${{ inputs.build_linux && inputs.build_pdf }}" == "true" ]]; then
echo "linux_pdf_vol1_artifact=${TARGET}-pdf-vol1-linux" >> $GITHUB_OUTPUT
echo "linux_pdf_vol2_artifact=${TARGET}-pdf-vol2-linux" >> $GITHUB_OUTPUT
fi
if [[ "${{ inputs.build_linux && inputs.build_epub }}" == "true" ]]; then
echo "linux_epub_vol1_artifact=${TARGET}-epub-vol1-linux" >> $GITHUB_OUTPUT
echo "linux_epub_vol2_artifact=${TARGET}-epub-vol2-linux" >> $GITHUB_OUTPUT
fi
if [[ "${{ inputs.build_windows && inputs.build_html }}" == "true" ]]; then
echo "windows_html_vol1_artifact=${TARGET}-html-vol1-windows" >> $GITHUB_OUTPUT
echo "windows_html_vol2_artifact=${TARGET}-html-vol2-windows" >> $GITHUB_OUTPUT
fi
if [[ "${{ inputs.build_windows && inputs.build_pdf }}" == "true" ]]; then
echo "windows_pdf_vol1_artifact=${TARGET}-pdf-vol1-windows" >> $GITHUB_OUTPUT
echo "windows_pdf_vol2_artifact=${TARGET}-pdf-vol2-windows" >> $GITHUB_OUTPUT
fi
if [[ "${{ inputs.build_windows && inputs.build_epub }}" == "true" ]]; then
echo "windows_epub_vol1_artifact=${TARGET}-epub-vol1-windows" >> $GITHUB_OUTPUT
echo "windows_epub_vol2_artifact=${TARGET}-epub-vol2-windows" >> $GITHUB_OUTPUT
fi
echo "✅ Results collected - Status: $BUILD_SUCCESS_MSG"