Files
cs249r_book/.github/workflows/book-validate-dev.yml

599 lines
26 KiB
YAML

name: '📚 Book · ✅ Validate (Dev)'
# =============================================================================
# ✅ Validate (Dev) — Lint, build, and test the book on every dev push
# =============================================================================
# Re-run trigger: 2026-05-18 — refresh after projects.json design-grammar fix
# =============================================================================
#
# Runs pre-commit checks, then builds both volumes in all formats (HTML, PDF,
# EPUB) on Linux and Windows using containerized Quarto. Uploads artifacts
# for the Preview workflow to deploy.
#
# Flow:
# 1. PRE_COMMIT — Run pre-commit hooks on all files
# 2. BUILD_CONFIG — Determine formats, OS targets, and container settings
# 3. CONTAINER_BUILD — Matrix build via book-build-container.yml (up to 12 jobs)
# 4. COLLECT_RESULTS — Aggregate per-platform success and artifact names
# 5. VALIDATION_SUMMARY — Report pass/fail and trigger preview deployment
#
# Triggers:
# - push: dev branch, paths under book/ and workflow files
# - workflow_dispatch: manual with format/OS/container options
#
# Deploys to: N/A (artifacts consumed by book-preview-dev.yml)
#
# Related:
# - book-build-container.yml — Reusable build matrix called by this workflow
# - book-preview-dev.yml — Deploys artifacts produced here to staging
# - book-publish-live.yml — Production pipeline (builds from main)
#
# =============================================================================
on:
push:
branches: [dev] # Auto-trigger for dev branch
paths:
- 'book/**'
- '.github/workflows/book-validate-dev.yml'
- '.github/workflows/book-preview-dev.yml'
workflow_dispatch: # Manual trigger for testing
inputs:
test_branch:
description: 'Branch to test (for feature branch testing)'
required: false
default: ''
type: string
target:
description: 'Target branch for build artifacts (dev/main)'
required: false
type: choice
default: 'dev'
options:
- dev
- main
# Format & OS selection
build_formats:
description: '📦 Build formats (all, html, pdf, epub)'
required: true
default: 'all'
type: choice
options:
- all
- html
- pdf
- epub
build_os:
description: '⚙️ Build OS (linux, both, windows) — Windows disabled by default'
required: true
default: 'linux'
type: choice
options:
- linux
- both
- windows
# Container validation
run_health_check:
description: '💊 Run container health check'
required: false
default: false
type: boolean
# Container configuration (consistent with other workflows)
container_registry:
description: 'Container registry URL'
required: false
default: 'ghcr.io'
type: string
container_tag:
description: 'Container tag to use'
required: false
default: 'latest'
type: string
permissions:
contents: write
pages: write
packages: read
actions: read
concurrency:
# Group includes event_name so a manual workflow_dispatch doesn't cancel
# the auto push run (or vice versa) on the same SHA.
group: book-validate-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
# Step 1: Pre-commit validation
pre-commit:
name: '🔍 Pre-commit Checks'
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ inputs.test_branch || github.ref }}
fetch-depth: 0
- name: 🐍 Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: 🔍 Run pre-commit checks
run: |
echo "🔍 Installing and running pre-commit..."
python -m pip install --upgrade pip
pip install pre-commit
echo "📦 Installing project dependencies for custom hooks..."
pip install -r requirements.txt || echo "⚠️ Could not install all requirements, continuing..."
echo "📦 Installing fallback hook dependencies..."
pip install rich requests pillow pint pandas pytest || echo "⚠️ Could not install one or more fallback deps, continuing..."
echo "🔧 Installing pre-commit hooks..."
pre-commit install --install-hooks
echo "🔄 Running pre-commit on all files..."
echo "Python version: $(python --version)"
if pre-commit run --all-files --verbose; then
echo "✅ Pre-commit checks: PASSED"
else
echo "❌ Pre-commit checks: FAILED"
echo "📋 Showing detailed output:"
pre-commit run --all-files --verbose || true
exit 1
fi
# Step 2: Container Health Check (validate containers before builds)
container-health-check:
name: '💊 Container Health Check'
needs: [build-config]
if: inputs.run_health_check == true
uses: ./.github/workflows/infra-health-check.yml # Shared workflow - no book- prefix
with:
test_linux: true
test_windows: true
container_registry: 'ghcr.io'
container_tag: 'latest'
# Step 3: Update contributors (after validation passes, before build matrix)
update-contributors:
name: '👥 Update Contributors'
needs: [pre-commit]
if: needs.pre-commit.result == 'success' && github.event_name == 'push'
uses: ./.github/workflows/update-contributors.yml
# Step 4: Determine build configuration (shared logic)
build-config:
name: '🎯 Build Configuration'
needs: [pre-commit, update-contributors]
if: always() && needs.pre-commit.result == 'success'
runs-on: ubuntu-latest
outputs:
build_html: ${{ steps.config.outputs.build_html }}
build_pdf: ${{ steps.config.outputs.build_pdf }}
build_epub: ${{ steps.config.outputs.build_epub }}
build_linux: ${{ steps.config.outputs.build_linux }}
build_windows: ${{ steps.config.outputs.build_windows }}
container_registry: ${{ steps.config.outputs.container_registry }}
container_tag: ${{ steps.config.outputs.container_tag }}
target: ${{ steps.config.outputs.target }}
steps:
- name: 🎯 Determine build configuration
id: config
run: |
echo "🎯 === BUILD CONFIGURATION ==="
# Determine what to build based on inputs (individual format flags)
if [ "${{ github.event_name }}" = "push" ]; then
# Auto-trigger: build Linux only. Windows validate-dev is temporarily
# disabled on push — the Windows container itself is still built by
# infra-container-windows.yml on its own schedule/path triggers.
# Re-enable by flipping BUILD_WINDOWS back to "true" below.
BUILD_HTML="true"
BUILD_PDF="true"
BUILD_EPUB="true"
BUILD_LINUX="true"
BUILD_WINDOWS="false"
TARGET="dev" # For push events, always target dev
echo "📊 Trigger: Automatic (dev branch push)"
echo "📊 HTML: $BUILD_HTML (mandatory)"
echo "📊 PDF: $BUILD_PDF (mandatory)"
echo "📊 EPUB: $BUILD_EPUB (mandatory)"
echo "📊 Linux: $BUILD_LINUX (mandatory)"
echo "📊 Windows: $BUILD_WINDOWS (temporarily disabled on dev push)"
echo "📊 Method: container-only"
echo "📊 Target: $TARGET"
else
# Manual trigger: respect choices
echo "📊 Trigger: Manual workflow dispatch"
BUILD_HTML="false"
if [[ "${{ inputs.build_formats }}" == "all" || "${{ inputs.build_formats }}" == "html" ]]; then
BUILD_HTML="true"
fi
BUILD_PDF="false"
if [[ "${{ inputs.build_formats }}" == "all" || "${{ inputs.build_formats }}" == "pdf" ]]; then
BUILD_PDF="true"
fi
BUILD_EPUB="false"
if [[ "${{ inputs.build_formats }}" == "all" || "${{ inputs.build_formats }}" == "epub" ]]; then
BUILD_EPUB="true"
fi
BUILD_LINUX="false"
if [[ "${{ inputs.build_os }}" == "both" || "${{ inputs.build_os }}" == "linux" ]]; then
BUILD_LINUX="true"
fi
BUILD_WINDOWS="false"
if [[ "${{ inputs.build_os }}" == "both" || "${{ inputs.build_os }}" == "windows" ]]; then
BUILD_WINDOWS="true"
fi
TARGET="${{ inputs.target }}"
echo "📊 Formats: ${{ inputs.build_formats }} (HTML: $BUILD_HTML, PDF: $BUILD_PDF)"
echo "📊 OS: ${{ inputs.build_os }} (Linux: $BUILD_LINUX, Windows: $BUILD_WINDOWS)"
echo "📊 Method: container-only"
echo "📊 Target: $TARGET"
fi
# Container configuration
CONTAINER_REGISTRY="${{ inputs.container_registry || 'ghcr.io' }}"
CONTAINER_TAG="${{ inputs.container_tag || 'latest' }}"
echo "📊 Final Configuration:"
echo " HTML: $BUILD_HTML"
echo " PDF: $BUILD_PDF"
echo " Linux: $BUILD_LINUX"
echo " Windows: $BUILD_WINDOWS"
echo " Method: container"
echo " Registry: $CONTAINER_REGISTRY"
echo " Tag: $CONTAINER_TAG"
echo " Target: $TARGET"
# Set outputs (individual format flags for matrix-driven approach)
echo "build_html=$BUILD_HTML" >> $GITHUB_OUTPUT
echo "build_pdf=$BUILD_PDF" >> $GITHUB_OUTPUT
echo "build_epub=$BUILD_EPUB" >> $GITHUB_OUTPUT
echo "build_linux=$BUILD_LINUX" >> $GITHUB_OUTPUT
echo "build_windows=$BUILD_WINDOWS" >> $GITHUB_OUTPUT
echo "container_registry=$CONTAINER_REGISTRY" >> $GITHUB_OUTPUT
echo "container_tag=$CONTAINER_TAG" >> $GITHUB_OUTPUT
echo "target=$TARGET" >> $GITHUB_OUTPUT
# Step 4: Matrix-driven build calls (clean and simple!)
build-container:
name: '🐳 Container Build Matrix'
needs: [build-config]
if: |
always() &&
(needs.build-config.result == 'success')
uses: ./.github/workflows/book-build-container.yml
with:
build_linux: ${{ needs.build-config.outputs.build_linux == 'true' }}
build_windows: ${{ needs.build-config.outputs.build_windows == 'true' }}
build_html: ${{ needs.build-config.outputs.build_html == 'true' }}
build_pdf: ${{ needs.build-config.outputs.build_pdf == 'true' }}
build_epub: ${{ needs.build-config.outputs.build_epub == 'true' }}
build_target: all
target: ${{ needs.build-config.outputs.target }}
container_registry: ${{ needs.build-config.outputs.container_registry }}
container_tag: ${{ needs.build-config.outputs.container_tag }}
# Step 4: Collect build results (needed for outputs)
collect-results:
name: '📊 Collect Results'
needs: [build-config, build-container]
runs-on: ubuntu-latest
if: always()
outputs:
linux_success: ${{ steps.collect.outputs.linux_success }}
windows_success: ${{ steps.collect.outputs.windows_success }}
linux_html_vol1_artifact: ${{ steps.collect.outputs.linux_html_vol1_artifact }}
linux_html_vol2_artifact: ${{ steps.collect.outputs.linux_html_vol2_artifact }}
linux_pdf_vol1_artifact: ${{ steps.collect.outputs.linux_pdf_vol1_artifact }}
linux_pdf_vol2_artifact: ${{ steps.collect.outputs.linux_pdf_vol2_artifact }}
linux_epub_vol1_artifact: ${{ steps.collect.outputs.linux_epub_vol1_artifact }}
linux_epub_vol2_artifact: ${{ steps.collect.outputs.linux_epub_vol2_artifact }}
steps:
- name: 📊 Collect build results
id: collect
run: |
echo "📊 === COLLECTING BUILD RESULTS ==="
# These will hold the final results, preferring container builds
OVERALL_SUCCESS="false"
LINUX_HTML_VOL1=""
LINUX_HTML_VOL2=""
LINUX_PDF_VOL1=""
LINUX_PDF_VOL2=""
LINUX_EPUB_VOL1=""
LINUX_EPUB_VOL2=""
# Container-only results
if [[ "${{ needs.build-container.result }}" == "success" ]]; then
echo "✅ Container builds completed successfully."
OVERALL_SUCCESS="${{ needs.build-container.outputs.build_success }}"
LINUX_HTML_VOL1="${{ needs.build-container.outputs.linux_html_vol1_artifact }}"
LINUX_HTML_VOL2="${{ needs.build-container.outputs.linux_html_vol2_artifact }}"
LINUX_PDF_VOL1="${{ needs.build-container.outputs.linux_pdf_vol1_artifact }}"
LINUX_PDF_VOL2="${{ needs.build-container.outputs.linux_pdf_vol2_artifact }}"
LINUX_EPUB_VOL1="${{ needs.build-container.outputs.linux_epub_vol1_artifact }}"
LINUX_EPUB_VOL2="${{ needs.build-container.outputs.linux_epub_vol2_artifact }}"
else
echo "❌ Container builds failed or were skipped."
OVERALL_SUCCESS="false"
fi
# Determine per-platform success based on overall success and which platforms were built
LINUX_SUCCESS="false"
if [[ "${{ needs.build-config.outputs.build_linux }}" == "true" && "$OVERALL_SUCCESS" == "true" ]]; then
LINUX_SUCCESS="true"
fi
WINDOWS_SUCCESS="false"
if [[ "${{ needs.build-config.outputs.build_windows }}" == "true" && "$OVERALL_SUCCESS" == "true" ]]; then
WINDOWS_SUCCESS="true"
fi
echo "Final Success Status: Linux -> $LINUX_SUCCESS, Windows -> $WINDOWS_SUCCESS"
# Set outputs
echo "linux_success=$LINUX_SUCCESS" >> $GITHUB_OUTPUT
echo "windows_success=$WINDOWS_SUCCESS" >> $GITHUB_OUTPUT
echo "linux_html_vol1_artifact=$LINUX_HTML_VOL1" >> $GITHUB_OUTPUT
echo "linux_html_vol2_artifact=$LINUX_HTML_VOL2" >> $GITHUB_OUTPUT
echo "linux_pdf_vol1_artifact=$LINUX_PDF_VOL1" >> $GITHUB_OUTPUT
echo "linux_pdf_vol2_artifact=$LINUX_PDF_VOL2" >> $GITHUB_OUTPUT
echo "linux_epub_vol1_artifact=$LINUX_EPUB_VOL1" >> $GITHUB_OUTPUT
echo "linux_epub_vol2_artifact=$LINUX_EPUB_VOL2" >> $GITHUB_OUTPUT
# Step 4a: Epubcheck validation (Tier 1 — blocks on FATAL)
# The pre-commit hook `book-epub-hygiene` catches source-level issues
# that historically caused EPUB rejection (SVG control chars, dupe
# marker ids, BibTeX URL escapes). Epubcheck is the full structural
# validator and catches the rest: OPF manifest shape, XHTML XML
# validity, cross-file references, URI syntax.
#
# Failure policy: FATAL > 0 fails the job (Kindle and ClearView
# reject any EPUB with FATAL, so this is a ship-stopper). ERROR > 0
# is reported as a warning but does NOT fail the job, to let
# remaining legacy errors be cleaned up incrementally without
# blocking feature work. Tighten this by setting MAX_ERRORS=0 once
# the baseline is clean in both volumes.
epub-validate:
name: '📘 EPUB validate (epubcheck via binder)'
needs: [collect-results]
if: |
always() &&
needs.collect-results.outputs.linux_success == 'true' &&
needs.collect-results.outputs.linux_epub_vol1_artifact != '' &&
needs.collect-results.outputs.linux_epub_vol2_artifact != ''
runs-on: ubuntu-latest
env:
# Ratchet baseline — fails if epubcheck counts exceed recorded
# per-volume ceilings. Update with
# ./binder check epub --scope epubcheck \
# --baseline book/tools/audit/epubcheck-baseline.json \
# --update-baseline
# after a cleanup lands, and commit the changed file in the
# same PR as the cleanup. This replaces the older flat
# MAX_FATAL/MAX_ERRORS approach: a ratchet prevents regression
# without cliff-failing during incremental cleanup.
#
# Path is relative to book/ because the run step does `cd book`
# before invoking the binder CLI.
BASELINE_PATH: 'tools/audit/epubcheck-baseline.json'
steps:
- name: 📥 Checkout repository
uses: actions/checkout@v6
- name: 🐍 Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: 📥 Download vol1 EPUB artifact
uses: actions/download-artifact@v8
with:
name: ${{ needs.collect-results.outputs.linux_epub_vol1_artifact }}
# Materialize into the canonical location the binder CLI
# discovers (_discover_built_epubs walks
# book/quarto/_build/epub-vol*/).
path: book/quarto/_build/epub-vol1
- name: 📥 Download vol2 EPUB artifact
uses: actions/download-artifact@v8
with:
name: ${{ needs.collect-results.outputs.linux_epub_vol2_artifact }}
path: book/quarto/_build/epub-vol2
- name: ☕ Set up Java (for the epubcheck jar)
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'
- name: ⬇️ Install book Python dependencies
# Installs the `epubcheck` PyPI wrapper (declared in
# book/tools/dependencies/requirements.txt) alongside rich,
# pyyaml, and the rest of the binder runtime. The wrapper
# bundles the epubcheck jar and exposes it on PATH.
run: |
python -m pip install --upgrade pip
pip install -r book/tools/dependencies/requirements.txt
- name: 📘 Run `binder check epub --scope epubcheck` with baseline ratchet
# Both source-level hygiene and post-build epubcheck validation
# live inside the binder CLI. This step invokes the same
# command a developer runs locally, so CI and local behaviour
# stay in lockstep.
#
# The --baseline ratchet means CI fails only when the per-
# volume FATAL/ERROR/WARNING count *exceeds* what was
# recorded last time the baseline was updated. Prevents
# regression without forcing a cliff-cleanup of every
# pre-existing issue on every PR.
env:
BASELINE_PATH: ${{ env.BASELINE_PATH }}
run: |
set -euo pipefail
cd book
# JSON pass: machine-readable artifact (uploaded below).
# GITHUB_ACTIONS=true is set automatically, so _epub_checks
# emits `::error` annotations inline on the PR diff.
./binder check epub --scope epubcheck \
--baseline "${BASELINE_PATH}" \
--json > epubcheck-report.json || true
# Human pass: exit code gates the CI job via the ratchet.
./binder check epub --scope epubcheck --baseline "${BASELINE_PATH}"
- name: 📤 Upload epubcheck report
if: always()
uses: actions/upload-artifact@v7
with:
name: epubcheck-report
path: book/epubcheck-report.json
retention-days: 14
# Step 4b: Link integrity (Tier 2 — non-blocking baseline)
# Tier 1 pre-commit (shared/scripts/check-internal-links.py) already blocks
# broken internal links + anchors. This Lychee pass adds external reachability
# as a warning. Flip fail_on_broken=true once the baseline is clean.
check-links:
name: '🔗 Check Links'
needs: [pre-commit]
if: always() && needs.pre-commit.result == 'success'
uses: ./.github/workflows/infra-link-check.yml
with:
path_pattern: './book/quarto/contents/**/*.qmd'
lycheeignore_path: 'book/config/linting/.lycheeignore'
fail_on_broken: false
max_concurrency: 10
# Step 5: Final results and summary
validation-summary:
name: '🎯 Validation Summary'
if: always()
needs: [pre-commit, build-config, collect-results, epub-validate, check-links]
runs-on: ubuntu-latest
steps:
- name: 🎯 Generate validation summary
run: |
echo "🎯 === VALIDATION SUMMARY ==="
echo "=============================="
echo ""
# Visual status indicators
echo "🔍 RESULTS:"
echo "----------"
if [ "${{ needs.pre-commit.result }}" = "success" ]; then
echo "✅ Pre-commit checks: PASSED"
else
echo "❌ Pre-commit checks: FAILED"
fi
if [ "${{ needs.build-config.outputs.build_linux }}" = "true" ]; then
if [ "${{ needs.collect-results.outputs.linux_success }}" = "true" ]; then
echo "✅ Linux build: PASSED"
else
echo "❌ Linux build: FAILED"
fi
else
echo "⏭️ Linux build: SKIPPED"
fi
if [ "${{ needs.build-config.outputs.build_windows }}" = "true" ]; then
if [ "${{ needs.collect-results.outputs.windows_success }}" = "true" ]; then
echo "✅ Windows build: PASSED"
else
echo "❌ Windows build: FAILED"
fi
else
echo "⏭️ Windows build: SKIPPED"
fi
case "${{ needs.epub-validate.result }}" in
success) echo "✅ EPUB validate (epubcheck): PASSED" ;;
failure) echo "❌ EPUB validate (epubcheck): FAILED" ;;
skipped) echo "⏭️ EPUB validate (epubcheck): SKIPPED" ;;
*) echo "⚠️ EPUB validate (epubcheck): ${{ needs.epub-validate.result }}" ;;
esac
echo ""
echo "🎯 OVERALL STATUS:"
echo "-----------------"
# Determine overall success
PRECOMMIT_OK="${{ needs.pre-commit.result == 'success' }}"
LINUX_OK="${{ needs.collect-results.outputs.linux_success == 'true' || needs.build-config.outputs.build_linux == 'false' }}"
WINDOWS_OK="${{ needs.collect-results.outputs.windows_success == 'true' || needs.build-config.outputs.build_windows == 'false' }}"
if [ "$PRECOMMIT_OK" = "true" ] && [ "$LINUX_OK" = "true" ] && [ "$WINDOWS_OK" = "true" ]; then
echo "🟢 VALIDATION: FULLY OPERATIONAL ✅"
echo " All enabled checks passed successfully!"
echo "🚀 PREVIEW: DEPLOYMENT TRIGGERED ✅"
echo " A separate workflow will deploy the preview to the staging site."
else
echo "🔴 VALIDATION: ISSUES DETECTED ❌"
echo " Check the logs above for details"
fi
echo "=============================="
- name: 📋 Generate GitHub summary
run: |
echo "# 🎯 Enhanced Dev Validation Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## 📊 Build Results" >> $GITHUB_STEP_SUMMARY
echo "- **Pre-commit**: ${{ needs.pre-commit.result }}" >> $GITHUB_STEP_SUMMARY
echo "- **Linux Build**: ${{ needs.collect-results.outputs.linux_success == 'true' && '✅ success' || '❌ failed' }}" >> $GITHUB_STEP_SUMMARY
echo "- **Windows Build**: ${{ needs.collect-results.outputs.windows_success == 'true' && '✅ success' || '❌ failed' }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## 🔧 Build Configuration" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.event_name }}" = "push" ]; then
echo "- **Trigger**: Automatic (dev branch push)" >> $GITHUB_STEP_SUMMARY
echo "- **Format**: HTML + PDF + EPUB (mandatory)" >> $GITHUB_STEP_SUMMARY
echo "- **OS**: Linux only (Windows temporarily disabled on push)" >> $GITHUB_STEP_SUMMARY
echo "- **Method**: ✅ Container builds only" >> $GITHUB_STEP_SUMMARY
else
echo "- **Trigger**: Manual workflow dispatch" >> $GITHUB_STEP_SUMMARY
echo "- **Formats**: ${{ inputs.build_formats }}" >> $GITHUB_STEP_SUMMARY
echo "- **OS**: ${{ inputs.build_os }}" >> $GITHUB_STEP_SUMMARY
echo "- **Method**: ✅ Container builds only" >> $GITHUB_STEP_SUMMARY
echo "- **Registry**: ${{ needs.build-config.outputs.container_registry }}" >> $GITHUB_STEP_SUMMARY
echo "- **Tag**: ${{ needs.build-config.outputs.container_tag }}" >> $GITHUB_STEP_SUMMARY
echo "- **Target**: ${{ needs.build-config.outputs.target }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "## 🚀 Preview Deployment" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.collect-results.outputs.linux_success }}" = "true" ]; then
echo "Deployment will be handled by a separate workflow." >> $GITHUB_STEP_SUMMARY
echo "Check the 'Actions' tab for the '🚀 Deploy Preview' workflow run." >> $GITHUB_STEP_SUMMARY
else
echo "Deployment skipped due to build failure." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "## 🎯 Summary" >> $GITHUB_STEP_SUMMARY
LINUX_OK="${{ needs.collect-results.outputs.linux_success == 'true' || needs.build-config.outputs.build_linux == 'false' }}"
WINDOWS_OK="${{ needs.collect-results.outputs.windows_success == 'true' || needs.build-config.outputs.build_windows == 'false' }}"
if [ "${{ needs.pre-commit.result }}" = "success" ] && [ "$LINUX_OK" = "true" ] && [ "$WINDOWS_OK" = "true" ]; then
echo "✅ **Validation Passed** - All enabled builds successful" >> $GITHUB_STEP_SUMMARY
echo "🚀 **Preview Deployment Triggered** - A separate workflow is handling the deployment." >> $GITHUB_STEP_SUMMARY
else
echo "❌ **Validation Failed** - Check the logs above" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "## 🔗 Link Check (non-blocking)" >> $GITHUB_STEP_SUMMARY
echo "- Result: \`${{ needs.check-links.result }}\` (see job for detail)" >> $GITHUB_STEP_SUMMARY