fix(vol4): clean inline math delimiters in 01-boundary, 08-perception⦠#712
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: | |
| # - pull_request into dev, paths under book/ and workflow files | |
| # - push: dev branch, paths under book/ and workflow files | |
| # - workflow_dispatch: manual with format/OS/container options | |
| # | |
| # The pull_request trigger exists because book/** changes previously merged | |
| # with no render gate. PR #2002 landed with the vol1 chapter list commented | |
| # out (the PDF built the foreword alone) and with \newif\ifmlsyscoverpage | |
| # declared in a vol1-only header while the shared before-body.tex set it, | |
| # which broke the Volume II PDF build outright. Neither was visible until | |
| # someone built the PDFs by hand days later. | |
| # | |
| # 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: | |
| pull_request: | |
| branches: [dev] # Render gate: catch book breakage before it lands | |
| paths: | |
| - 'book/**' | |
| - 'books/**' | |
| - 'publishing/**' | |
| - '.github/workflows/book-validate-dev.yml' | |
| - '.github/workflows/book-build-container.yml' | |
| - '.github/workflows/book-preview-dev.yml' | |
| - '.github/workflows/update-contributors.yml' | |
| push: | |
| branches: [dev] # Auto-trigger for dev branch | |
| paths: | |
| - 'book/**' | |
| - 'books/**' | |
| - 'publishing/**' | |
| - '.github/workflows/book-validate-dev.yml' | |
| - '.github/workflows/book-build-container.yml' | |
| - '.github/workflows/book-preview-dev.yml' | |
| - '.github/workflows/update-contributors.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 | |
| # Target selection | |
| build_target: | |
| description: 'π¦ Build target (vol1, vol2, vol3, vol4, all)' | |
| required: true | |
| default: 'all' | |
| type: choice | |
| options: | |
| - all | |
| - vol1 | |
| - vol2 | |
| - vol3 | |
| - vol4 | |
| # Format & OS selection | |
| build_formats: | |
| description: 'π¦ Build formats (all, html, pdf, epub)' | |
| required: true | |
| default: 'all' | |
| type: choice | |
| options: | |
| - all | |
| - html | |
| - 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 "Python version: $(python --version)" | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| echo "π Running pre-commit on PR changes (origin/${{ github.base_ref }}..HEAD)..." | |
| CMD="pre-commit run --from-ref origin/${{ github.base_ref }} --to-ref HEAD --verbose" | |
| elif [ "${{ github.event_name }}" = "push" ] && git rev-parse --verify HEAD~1 >/dev/null 2>&1; then | |
| echo "π Running pre-commit on push changes (HEAD~1..HEAD)..." | |
| CMD="pre-commit run --from-ref HEAD~1 --to-ref HEAD --verbose" | |
| else | |
| echo "π Running pre-commit on all files..." | |
| CMD="pre-commit run --all-files --verbose" | |
| fi | |
| if eval "$CMD"; then | |
| echo "β Pre-commit checks: PASSED" | |
| else | |
| echo "β Pre-commit checks: FAILED" | |
| 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 (runs in parallel with builds after pre-commit passes) | |
| 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] | |
| 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 }} | |
| build_target: ${{ steps.config.outputs.build_target }} | |
| container_registry: ${{ steps.config.outputs.container_registry }} | |
| container_tag: ${{ steps.config.outputs.container_tag }} | |
| target: ${{ steps.config.outputs.target }} | |
| steps: | |
| - name: π₯ Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 2 | |
| - 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" ] || [ "${{ github.event_name }}" = "pull_request" ]; then | |
| BUILD_HTML="true" | |
| BUILD_PDF="true" | |
| BUILD_EPUB="true" | |
| BUILD_LINUX="true" | |
| BUILD_WINDOWS="false" | |
| TARGET="dev" | |
| # Selective auto-detection of modified volume paths | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }} HEAD 2>/dev/null || echo "") | |
| else | |
| CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "") | |
| fi | |
| echo "π Changed files: $CHANGED_FILES" | |
| HAS_VOL1=false | |
| HAS_VOL2=false | |
| HAS_VOL3=false | |
| HAS_VOL4=false | |
| HAS_SHARED=false | |
| while IFS= read -r file; do | |
| [ -z "$file" ] && continue | |
| if [[ "$file" =~ ^books/vol1/ ]] || [[ "$file" =~ ^publishing/quarto/contents/vol1/ ]]; then HAS_VOL1=true; fi | |
| if [[ "$file" =~ ^books/vol2/ ]] || [[ "$file" =~ ^publishing/quarto/contents/vol2/ ]]; then HAS_VOL2=true; fi | |
| if [[ "$file" =~ ^books/vol3/ ]] || [[ "$file" =~ ^publishing/quarto/contents/vol3/ ]]; then HAS_VOL3=true; fi | |
| if [[ "$file" =~ ^books/vol4/ ]] || [[ "$file" =~ ^publishing/quarto/contents/vol4/ ]]; then HAS_VOL4=true; fi | |
| if [[ "$file" =~ ^publishing/binder ]] || [[ "$file" =~ ^shared/ ]] || [[ "$file" =~ ^\.github/ ]]; then HAS_SHARED=true; fi | |
| done <<< "$CHANGED_FILES" | |
| if [ "$HAS_SHARED" = true ]; then | |
| BUILD_TARGET="all" | |
| elif [ "$HAS_VOL1" = true ] && [ "$HAS_VOL2" = false ] && [ "$HAS_VOL3" = false ] && [ "$HAS_VOL4" = false ]; then | |
| BUILD_TARGET="vol1" | |
| elif [ "$HAS_VOL2" = true ] && [ "$HAS_VOL1" = false ] && [ "$HAS_VOL3" = false ] && [ "$HAS_VOL4" = false ]; then | |
| BUILD_TARGET="vol2" | |
| elif [ "$HAS_VOL3" = true ] && [ "$HAS_VOL1" = false ] && [ "$HAS_VOL2" = false ] && [ "$HAS_VOL4" = false ]; then | |
| BUILD_TARGET="vol3" | |
| elif [ "$HAS_VOL4" = true ] && [ "$HAS_VOL1" = false ] && [ "$HAS_VOL2" = false ] && [ "$HAS_VOL3" = false ]; then | |
| BUILD_TARGET="vol4" | |
| else | |
| BUILD_TARGET="all" | |
| fi | |
| echo "π Trigger: Automatic ($BUILD_TARGET detected)" | |
| else | |
| # Manual trigger: respect choices | |
| BUILD_TARGET="${{ inputs.build_target || 'all' }}" | |
| echo "π Trigger: Manual workflow dispatch ($BUILD_TARGET)" | |
| 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 }}" | |
| fi | |
| # Container configuration | |
| CONTAINER_REGISTRY="${{ inputs.container_registry || 'ghcr.io' }}" | |
| CONTAINER_TAG="${{ inputs.container_tag || 'latest' }}" | |
| echo "π Final Configuration:" | |
| echo " Target Volume: $BUILD_TARGET" | |
| echo " HTML: $BUILD_HTML" | |
| echo " PDF: $BUILD_PDF" | |
| echo " EPUB: $BUILD_EPUB" | |
| echo " Linux: $BUILD_LINUX" | |
| echo " Windows: $BUILD_WINDOWS" | |
| echo " Registry: $CONTAINER_REGISTRY" | |
| echo " Tag: $CONTAINER_TAG" | |
| echo " Target: $TARGET" | |
| # Set outputs | |
| echo "build_target=$BUILD_TARGET" >> $GITHUB_OUTPUT | |
| 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: ${{ needs.build-config.outputs.build_target }} | |
| 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: π₯ Checkout repository | |
| uses: actions/checkout@v4 | |
| - 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 | |
| - name: π·οΈ Update and Publish per-volume status badges | |
| if: always() | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| GITHUB_RUN_ID: ${{ github.run_id }} | |
| run: | | |
| python3 site/scripts/update_volume_status.py --publish-gh-pages |