docs-artifact-ready #160
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
| # Build and Deploy | |
| # | |
| # Builds the Zensical site and aggregates documentation from execution-specs, | |
| # then deploys as a combined site to GitHub Pages. | |
| # | |
| # Triggers: | |
| # - Push to main: Rebuilds when site content changes | |
| # - repository_dispatch: Triggered by execution-specs when docs are updated | |
| # - schedule: Daily rebuild to catch any missed updates | |
| # - workflow_dispatch: Manual trigger for testing/recovery | |
| # | |
| # Site structure at steel.ethereum.foundation: | |
| # / - Zensical site | |
| # /docs/ - Default branch docs (mirrored) | |
| # /docs/spec/ - Default branch spec (mirrored) | |
| # /docs/<branch>/ - Branch-specific docs | |
| # /docs/<branch>/spec - Branch-specific spec | |
| # /docs/versions.json - Version manifest | |
| name: Build and Deploy | |
| on: | |
| push: | |
| branches: | |
| - main | |
| # Triggered by execution-specs docs-producer workflow | |
| repository_dispatch: | |
| types: [docs-artifact-ready] | |
| # Manual trigger for testing or recovery | |
| workflow_dispatch: | |
| inputs: | |
| skip_branches: | |
| description: "Comma-separated branches to skip (for debugging)" | |
| required: false | |
| type: string | |
| skip_docs: | |
| description: "Skip docs aggregation (deploy Zensical only)" | |
| required: false | |
| type: boolean | |
| default: false | |
| # Scheduled fallback for recovery from transient failures | |
| schedule: | |
| - cron: "0 4 * * *" # Daily at 4 AM UTC | |
| concurrency: | |
| group: deploy | |
| cancel-in-progress: false # Don't cancel in-progress deploys | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| env: | |
| # Source repository for docs artifacts | |
| SOURCE_REPO: ethereum/execution-specs | |
| # Branch configuration - must match execution-specs allowlist | |
| # Format: branch_path|label | |
| BRANCH_CONFIG: | | |
| experiments/publish-docs|Amsterdam | |
| experiments/publish-docs-test|Amsterdam (Test) | |
| # Default branch to mirror to root of /docs/ | |
| DEFAULT_BRANCH: experiments/publish-docs | |
| jobs: | |
| deploy: | |
| name: "Build and Deploy" | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Log trigger information | |
| run: | | |
| echo "=== Trigger Information ===" | |
| echo "Event: ${{ github.event_name }}" | |
| if [ "${{ github.event_name }}" = "repository_dispatch" ]; then | |
| echo "Dispatch payload:" | |
| echo ' branch: ${{ github.event.client_payload.branch }}' | |
| echo ' sha: ${{ github.event.client_payload.sha }}' | |
| echo ' run_id: ${{ github.event.client_payload.run_id }}' | |
| fi | |
| echo "" | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup Pages | |
| uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 | |
| with: | |
| enable-cache: false | |
| version: ${{ vars.UV_VERSION }} | |
| python-version: ${{ vars.DEFAULT_PYTHON_VERSION }} | |
| # Build the main Zensical site | |
| - name: Build Zensical site | |
| run: | | |
| uv run zensical build --clean | |
| echo "Zensical site built to site/" | |
| # --- Docs Aggregation --- | |
| - name: Initialize docs staging | |
| if: github.event.inputs.skip_docs != 'true' | |
| run: | | |
| mkdir -p site/docs | |
| mkdir -p artifacts | |
| - name: Parse branch configuration | |
| if: github.event.inputs.skip_docs != 'true' | |
| id: config | |
| run: | | |
| BRANCHES="" | |
| while IFS='|' read -r path _; do | |
| if [ -n "$path" ]; then | |
| BRANCHES="$BRANCHES $path" | |
| fi | |
| done <<< "$BRANCH_CONFIG" | |
| BRANCHES=$(echo "$BRANCHES" | xargs) | |
| echo "branches=$BRANCHES" >> $GITHUB_OUTPUT | |
| echo "Configured branches: $BRANCHES" | |
| - name: Download artifacts from execution-specs | |
| if: github.event.inputs.skip_docs != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.EXECUTION_SPECS_READ_TOKEN }} | |
| run: | | |
| SKIP_BRANCHES="${{ github.event.inputs.skip_branches }}" | |
| SUCCESSFUL_BRANCHES="" | |
| FAILED_BRANCHES="" | |
| for BRANCH in ${{ steps.config.outputs.branches }}; do | |
| if echo "$SKIP_BRANCHES" | grep -qF "$BRANCH"; then | |
| echo "Skipping branch (user requested): $BRANCH" | |
| continue | |
| fi | |
| BRANCH_SAFE=$(echo "$BRANCH" | tr '/' '-') | |
| ARTIFACT_NAME="docs-${BRANCH_SAFE}" | |
| echo "" | |
| echo "=== Processing branch: $BRANCH ===" | |
| # Find the latest successful workflow run for this branch | |
| RUN_ID=$(gh run list \ | |
| --repo "$SOURCE_REPO" \ | |
| --workflow "docs-build.yaml" \ | |
| --branch "$BRANCH" \ | |
| --status success \ | |
| --limit 1 \ | |
| --json databaseId \ | |
| --jq '.[0].databaseId' 2>/dev/null || echo "") | |
| if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then | |
| echo "WARNING: No successful run found for branch $BRANCH" | |
| FAILED_BRANCHES="$FAILED_BRANCHES $BRANCH" | |
| continue | |
| fi | |
| echo "Found run ID: $RUN_ID" | |
| if gh run download "$RUN_ID" \ | |
| --repo "$SOURCE_REPO" \ | |
| --name "$ARTIFACT_NAME" \ | |
| --dir "artifacts/$BRANCH_SAFE" 2>/dev/null; then | |
| echo "Downloaded artifact for $BRANCH" | |
| SUCCESSFUL_BRANCHES="$SUCCESSFUL_BRANCHES $BRANCH" | |
| else | |
| echo "WARNING: Failed to download artifact for branch $BRANCH" | |
| FAILED_BRANCHES="$FAILED_BRANCHES $BRANCH" | |
| fi | |
| done | |
| echo "successful_branches=$SUCCESSFUL_BRANCHES" >> $GITHUB_ENV | |
| echo "failed_branches=$FAILED_BRANCHES" >> $GITHUB_ENV | |
| echo "" | |
| echo "=== Download Summary ===" | |
| echo "Successful: $SUCCESSFUL_BRANCHES" | |
| echo "Failed: $FAILED_BRANCHES" | |
| - name: Stage branch artifacts | |
| if: github.event.inputs.skip_docs != 'true' | |
| run: | | |
| for BRANCH in $successful_branches; do | |
| BRANCH_SAFE=$(echo "$BRANCH" | tr '/' '-') | |
| ARTIFACT_DIR="artifacts/${BRANCH_SAFE}" | |
| if [ ! -d "$ARTIFACT_DIR" ]; then | |
| continue | |
| fi | |
| echo "=== Staging branch: $BRANCH ===" | |
| # Find the branch directory inside the artifact | |
| if [ -d "$ARTIFACT_DIR/$BRANCH" ]; then | |
| SOURCE_DIR="$ARTIFACT_DIR/$BRANCH" | |
| else | |
| SOURCE_DIR=$(find "$ARTIFACT_DIR" -mindepth 1 -maxdepth 1 -type d | head -1) | |
| fi | |
| if [ -z "$SOURCE_DIR" ] || [ ! -d "$SOURCE_DIR" ]; then | |
| echo "WARNING: Could not find content for branch $BRANCH" | |
| continue | |
| fi | |
| mkdir -p "site/docs/$(dirname "$BRANCH")" | |
| rsync -av "$SOURCE_DIR/" "site/docs/$BRANCH/" | |
| echo "Staged branch $BRANCH" | |
| done | |
| - name: Validate staged content | |
| if: github.event.inputs.skip_docs != 'true' | |
| id: validate | |
| run: | | |
| HAS_DEFAULT="false" | |
| for BRANCH in $successful_branches; do | |
| if [ -d "site/docs/$BRANCH" ]; then | |
| if [ "$BRANCH" = "$DEFAULT_BRANCH" ]; then | |
| HAS_DEFAULT="true" | |
| fi | |
| fi | |
| done | |
| echo "has_default=$HAS_DEFAULT" >> $GITHUB_OUTPUT | |
| if [ "$HAS_DEFAULT" = "false" ]; then | |
| echo "WARNING: Default branch $DEFAULT_BRANCH is not available" | |
| fi | |
| - name: Mirror default branch to root of /docs/ | |
| if: github.event.inputs.skip_docs != 'true' && steps.validate.outputs.has_default == 'true' | |
| run: | | |
| echo "Mirroring $DEFAULT_BRANCH to root of /docs/" | |
| rsync -av \ | |
| --exclude='spec/' \ | |
| --exclude='mainnet/' \ | |
| --exclude='forks/' \ | |
| --exclude='devnets/' \ | |
| --exclude='eips/' \ | |
| "site/docs/$DEFAULT_BRANCH/" site/docs/ | |
| mkdir -p site/docs/spec | |
| rsync -av "site/docs/$DEFAULT_BRANCH/spec/" site/docs/spec/ | |
| - name: Generate versions.json | |
| if: github.event.inputs.skip_docs != 'true' | |
| run: | | |
| cat > site/docs/versions.json <<EOF | |
| { | |
| "default": "${DEFAULT_BRANCH}", | |
| "versions": [ | |
| EOF | |
| FIRST=true | |
| while IFS='|' read -r path label; do | |
| if [ -z "$path" ] || [ ! -d "site/docs/$path" ]; then | |
| continue | |
| fi | |
| ALIASES="[]" | |
| if [ "$path" = "$DEFAULT_BRANCH" ]; then | |
| ALIASES='["latest"]' | |
| fi | |
| if [ "$FIRST" = "true" ]; then | |
| FIRST=false | |
| else | |
| echo "," >> site/docs/versions.json | |
| fi | |
| cat >> site/docs/versions.json <<ENTRY | |
| { | |
| "version": "$path", | |
| "title": "$label", | |
| "aliases": $ALIASES | |
| } | |
| ENTRY | |
| done <<< "$BRANCH_CONFIG" | |
| cat >> site/docs/versions.json <<'EOF' | |
| ] | |
| } | |
| EOF | |
| echo "Generated versions.json:" | |
| cat site/docs/versions.json | |
| - name: Copy versions.json to intermediate directories | |
| if: github.event.inputs.skip_docs != 'true' | |
| run: | | |
| # MkDocs Material's version selector looks for ../versions.json relative | |
| # to the current version directory. For multi-depth branch paths like | |
| # forks/amsterdam or devnets/amsterdam/2, we need to copy versions.json | |
| # to intermediate directories so the selector can find it. | |
| # | |
| # e.g., /docs/forks/amsterdam/ looks for /docs/forks/versions.json | |
| # /docs/devnets/amsterdam/2/ looks for /docs/devnets/amsterdam/versions.json | |
| while IFS='|' read -r path _; do | |
| if [ -z "$path" ] || [ ! -d "site/docs/$path" ]; then | |
| continue | |
| fi | |
| # Get parent directory of the branch path | |
| PARENT_DIR=$(dirname "$path") | |
| if [ "$PARENT_DIR" != "." ]; then | |
| # Copy versions.json to parent directory | |
| mkdir -p "site/docs/$PARENT_DIR" | |
| cp site/docs/versions.json "site/docs/$PARENT_DIR/versions.json" | |
| echo "Copied versions.json to site/docs/$PARENT_DIR/" | |
| # For deeper paths (e.g., devnets/amsterdam/2), also copy to grandparent | |
| GRANDPARENT_DIR=$(dirname "$PARENT_DIR") | |
| if [ "$GRANDPARENT_DIR" != "." ]; then | |
| mkdir -p "site/docs/$GRANDPARENT_DIR" | |
| cp site/docs/versions.json "site/docs/$GRANDPARENT_DIR/versions.json" | |
| echo "Copied versions.json to site/docs/$GRANDPARENT_DIR/" | |
| fi | |
| fi | |
| done <<< "$BRANCH_CONFIG" | |
| # --- End Docs Aggregation --- | |
| - name: Add .nojekyll | |
| run: | | |
| touch site/.nojekyll | |
| - name: Deployment summary | |
| run: | | |
| echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ -d "site/docs" ] && [ "$(ls -A site/docs 2>/dev/null)" ]; then | |
| echo "### Documentation Branches" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Branch | Label | Status | Size |" >> $GITHUB_STEP_SUMMARY | |
| echo "|--------|-------|--------|------|" >> $GITHUB_STEP_SUMMARY | |
| while IFS='|' read -r path label; do | |
| if [ -z "$path" ]; then continue; fi | |
| if [ -d "site/docs/$path" ]; then | |
| SIZE=$(du -sh "site/docs/$path" | cut -f1) | |
| DEFAULT_MARKER="" | |
| if [ "$path" = "$DEFAULT_BRANCH" ]; then | |
| DEFAULT_MARKER=" (default)" | |
| fi | |
| echo "| $path | $label | :white_check_mark:$DEFAULT_MARKER | $SIZE |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| $path | $label | :x: | - |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| done <<< "$BRANCH_CONFIG" | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "### Documentation" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "No documentation artifacts available." >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| TOTAL=$(du -sh site | cut -f1) | |
| echo "**Total site size:** $TOTAL" >> $GITHUB_STEP_SUMMARY | |
| - name: Upload Pages artifact | |
| uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 | |
| with: | |
| path: site/ | |
| - name: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 |