Actual real production #12
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: Hardware CI | |
| on: | |
| push: | |
| paths: | |
| - "hardware/**" | |
| - ".github/workflows/hardware.yml" | |
| - ".github/hardware/**" | |
| pull_request: | |
| paths: | |
| - "hardware/**" | |
| - ".github/workflows/hardware.yml" | |
| - ".github/hardware/**" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| project-matrix: | |
| name: Hardware project matrix | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.projects.outputs.matrix }} | |
| projects: ${{ steps.projects.outputs.projects }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Load hardware projects | |
| id: projects | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| projects="$(jq -c '.' .github/hardware/projects.json)" | |
| matrix="$(jq -c '{include: .}' .github/hardware/projects.json)" | |
| echo "projects=${projects}" >> "$GITHUB_OUTPUT" | |
| echo "matrix=${matrix}" >> "$GITHUB_OUTPUT" | |
| hardware-checks: | |
| name: ERC and DRC (${{ matrix.name }}) | |
| runs-on: ubuntu-latest | |
| needs: project-matrix | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.project-matrix.outputs.matrix) }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Run KiBot ERC and DRC | |
| id: run-kibot-checks | |
| continue-on-error: true | |
| uses: INTI-CMNB/KiBot@v2_k9 | |
| with: | |
| config: ${{ matrix.dir }}/${{ matrix.checks_config }} | |
| schema: ${{ matrix.dir }}/${{ matrix.schematic }} | |
| board: ${{ matrix.dir }}/${{ matrix.board }} | |
| dir: ${{ matrix.dir }}/output/checks | |
| - name: Upload ERC and DRC reports | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ matrix.slug }}-check-reports | |
| path: ${{ matrix.dir }}/output/checks | |
| if-no-files-found: warn | |
| - name: Fail if ERC or DRC failed | |
| if: ${{ steps.run-kibot-checks.outcome != 'success' }} | |
| shell: bash | |
| run: | | |
| echo "ERC/DRC failed for ${{ matrix.name }}." | |
| exit 1 | |
| hardware-outputs: | |
| name: Documentation and fabrication outputs (${{ matrix.name }}) | |
| runs-on: ubuntu-latest | |
| needs: | |
| - project-matrix | |
| - hardware-checks | |
| if: ${{ always() && needs.project-matrix.result == 'success' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.project-matrix.outputs.matrix) }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Generate KiCad outputs | |
| id: generate-hardware-outputs | |
| continue-on-error: true | |
| uses: INTI-CMNB/KiBot@v2_k9 | |
| with: | |
| config: ${{ matrix.dir }}/${{ matrix.outputs_config }} | |
| schema: ${{ matrix.dir }}/${{ matrix.schematic }} | |
| board: ${{ matrix.dir }}/${{ matrix.board }} | |
| dir: ${{ matrix.dir }}/output/generated | |
| - name: Upload generated hardware outputs | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ matrix.slug }}-hardware-outputs | |
| path: ${{ matrix.dir }}/output/generated | |
| if-no-files-found: warn | |
| - name: Fail if hardware output generation failed | |
| if: ${{ steps.generate-hardware-outputs.outcome != 'success' }} | |
| shell: bash | |
| run: | | |
| echo "Hardware output generation failed for ${{ matrix.name }}." | |
| exit 1 | |
| hardware-3d-model: | |
| name: 3D model (${{ matrix.name }}) | |
| runs-on: ubuntu-latest | |
| needs: | |
| - project-matrix | |
| - hardware-checks | |
| if: ${{ always() && needs.project-matrix.result == 'success' }} | |
| continue-on-error: true | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.project-matrix.outputs.matrix) }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Export VRML model with KiBot | |
| id: export-vrml | |
| continue-on-error: true | |
| uses: INTI-CMNB/KiBot@v2_k9 | |
| with: | |
| config: ${{ matrix.dir }}/${{ matrix.model_config }} | |
| schema: ${{ matrix.dir }}/${{ matrix.schematic }} | |
| board: ${{ matrix.dir }}/${{ matrix.board }} | |
| dir: ${{ matrix.dir }}/output/3d-source | |
| - name: Find exported VRML model | |
| id: find-vrml | |
| if: ${{ always() }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| model_dir="hardware-3d-artifacts/${{ matrix.slug }}/model" | |
| mkdir -p "$model_dir" | |
| mapfile -t board_vrml_candidates < <(find "${{ matrix.dir }}/output/3d-source" \( -iname '*.wrl' -o -iname '*.vrml' \) ! -path '*/shapes3D/*' -print 2>/dev/null | sort) | |
| if [ "${#board_vrml_candidates[@]}" -gt 0 ]; then | |
| vrml_path="${board_vrml_candidates[0]}" | |
| else | |
| vrml_path="$(find "${{ matrix.dir }}/output/3d-source" \( -iname '*.wrl' -o -iname '*.vrml' \) -print 2>/dev/null | sort | head -n 1 || true)" | |
| fi | |
| echo "path=${vrml_path}" >> "$GITHUB_OUTPUT" | |
| if [ -z "$vrml_path" ]; then | |
| { | |
| echo "No VRML model was generated for ${{ matrix.name }}." | |
| echo "KiBot VRML step outcome: ${{ steps.export-vrml.outcome }}" | |
| } > "$model_dir/README.txt" | |
| else | |
| echo "Selected VRML model: $vrml_path" | |
| cp "$vrml_path" "$model_dir/board-source.wrl" | |
| if [ -d "$(dirname "$vrml_path")/shapes3D" ]; then | |
| cp -a "$(dirname "$vrml_path")/shapes3D" "$model_dir/shapes3D" | |
| fi | |
| if ! python3 .github/hardware/3d/bundle_vrml.py "$vrml_path" "$model_dir/board.wrl" --copy-shapes "$model_dir/shapes3D"; then | |
| echo "VRML bundling failed for ${{ matrix.name }}; publishing source VRML with shapes3D fallback." | |
| cp "$model_dir/board-source.wrl" "$model_dir/board.wrl" | |
| { | |
| echo "VRML bundling failed for ${{ matrix.name }}." | |
| echo "The original VRML export is published as board.wrl with shapes3D fallback when available." | |
| } > "$model_dir/README.txt" | |
| fi | |
| fi | |
| - name: Upload 3D model outputs | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ matrix.slug }}-3d-model | |
| path: hardware-3d-artifacts/${{ matrix.slug }} | |
| if-no-files-found: warn | |
| build-pages-site: | |
| name: Build GitHub Pages site | |
| runs-on: ubuntu-latest | |
| needs: | |
| - project-matrix | |
| - hardware-checks | |
| - hardware-outputs | |
| - hardware-3d-model | |
| if: ${{ always() }} | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: ui/package-lock.json | |
| - name: Build UI Pages root | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| npm ci --prefix ui | |
| npm run build --prefix ui | |
| - name: Configure GitHub Pages | |
| uses: actions/configure-pages@v6 | |
| with: | |
| enablement: true | |
| - name: Download hardware artifacts | |
| uses: actions/download-artifact@v8 | |
| continue-on-error: true | |
| with: | |
| path: artifacts | |
| - name: Assemble static site | |
| shell: bash | |
| env: | |
| PROJECTS_JSON: ${{ needs.project-matrix.outputs.projects }} | |
| CHECKS_RESULT: ${{ needs.hardware-checks.result }} | |
| OUTPUTS_RESULT: ${{ needs.hardware-outputs.result }} | |
| MODELS_RESULT: ${{ needs.hardware-3d-model.result }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p site | |
| cp -a ui/dist/. site/ | |
| mkdir -p site/hardware/projects site/hardware/preview | |
| cp .github/hardware/site/style.css site/hardware/style.css | |
| cp .github/hardware/site/pr-preview.html site/hardware/preview/index.html | |
| cat > site/hardware/index.html <<EOF | |
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Hardware Boards</title> | |
| <link rel="stylesheet" href="style.css"> | |
| </head> | |
| <body> | |
| <header class="topbar"> | |
| <div> | |
| <p class="eyebrow">KiCad CI review</p> | |
| <h1>Hardware Boards</h1> | |
| </div> | |
| <nav aria-label="Project links"> | |
| <a href="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}">Actions run</a> | |
| </nav> | |
| </header> | |
| <main> | |
| <section class="status-band" aria-label="Build status"> | |
| <div> | |
| <span class="label">Run</span> | |
| <a href="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}">GitHub Actions</a> | |
| </div> | |
| <div> | |
| <span class="label">Commit</span> | |
| <code>${GITHUB_SHA:0:12}</code> | |
| </div> | |
| <div> | |
| <span class="label">ERC / DRC</span> | |
| <code>${CHECKS_RESULT:-unknown}</code> | |
| </div> | |
| <div> | |
| <span class="label">Outputs</span> | |
| <code>${OUTPUTS_RESULT:-unknown}</code> | |
| </div> | |
| <div> | |
| <span class="label">3D</span> | |
| <code>${MODELS_RESULT:-unknown}</code> | |
| </div> | |
| </section> | |
| <section class="section"> | |
| <div class="section-heading"> | |
| <h2>Boards</h2> | |
| <p>Select a board to review generated reports, fabrication files, PDFs, and 3D model output.</p> | |
| </div> | |
| <div class="board-grid"> | |
| EOF | |
| echo "$PROJECTS_JSON" | jq -c '.[]' | while read -r project; do | |
| slug="$(echo "$project" | jq -r '.slug')" | |
| name="$(echo "$project" | jq -r '.name')" | |
| project_dir="$(echo "$project" | jq -r '.dir')" | |
| project_file="$(echo "$project" | jq -r '.project')" | |
| schematic_file="$(echo "$project" | jq -r '.schematic')" | |
| board_file="$(echo "$project" | jq -r '.board')" | |
| basename="$(echo "$project" | jq -r '.basename')" | |
| project_site="site/hardware/projects/$slug" | |
| mkdir -p "$project_site/reports" "$project_site/outputs" "$project_site/kicad" "$project_site/model" | |
| cp .github/hardware/site/index.html "$project_site/index.html" | |
| cp .github/hardware/site/style.css "$project_site/style.css" | |
| escape_sed() { | |
| printf '%s' "$1" | sed 's/[\/&]/\\&/g' | |
| } | |
| sed -i \ | |
| -e "s/__PROJECT_NAME__/$(escape_sed "$name")/g" \ | |
| -e "s/__PROJECT_FILE__/$(escape_sed "$project_file")/g" \ | |
| -e "s/__SCHEMATIC_FILE__/$(escape_sed "$schematic_file")/g" \ | |
| -e "s/__BOARD_FILE__/$(escape_sed "$board_file")/g" \ | |
| -e "s/__BASE_NAME__/$(escape_sed "$basename")/g" \ | |
| "$project_site/index.html" | |
| cp "$project_dir/$project_file" "$project_site/kicad/" | |
| cp "$project_dir/$schematic_file" "$project_site/kicad/" | |
| cp "$project_dir/$board_file" "$project_site/kicad/" | |
| if [ -d "artifacts/$slug-check-reports" ]; then | |
| cp -a "artifacts/$slug-check-reports/." "$project_site/reports/" | |
| fi | |
| if [ -d "artifacts/$slug-hardware-outputs" ]; then | |
| cp -a "artifacts/$slug-hardware-outputs/." "$project_site/outputs/" | |
| fi | |
| if [ -d "artifacts/$slug-3d-model/model" ]; then | |
| cp -a "artifacts/$slug-3d-model/model/." "$project_site/model/" | |
| fi | |
| cat > "$project_site/run-info.js" <<EOF | |
| window.HARDWARE_CI = { | |
| runUrl: "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}", | |
| sha: "${GITHUB_SHA}", | |
| checksResult: "${CHECKS_RESULT:-unknown}", | |
| outputsResult: "${OUTPUTS_RESULT:-unknown}", | |
| modelsResult: "${MODELS_RESULT:-unknown}", | |
| projectName: "${name}", | |
| projectSlug: "${slug}", | |
| projectFile: "${project_file}", | |
| schematicFile: "${schematic_file}", | |
| boardFile: "${board_file}", | |
| baseName: "${basename}" | |
| }; | |
| EOF | |
| if [ ! -f "$project_site/reports/$basename-erc.html" ]; then | |
| printf '<!doctype html><title>ERC report unavailable</title><h1>ERC report unavailable</h1><p>No ERC report artifact was produced for this run.</p>' > "$project_site/reports/$basename-erc.html" | |
| fi | |
| if [ ! -f "$project_site/reports/$basename-drc.html" ]; then | |
| printf '<!doctype html><title>DRC report unavailable</title><h1>DRC report unavailable</h1><p>No DRC report artifact was produced for this run.</p>' > "$project_site/reports/$basename-drc.html" | |
| fi | |
| cat >> site/hardware/index.html <<EOF | |
| <a class="board-card" href="projects/$slug/"> | |
| <span class="board-name">$name</span> | |
| <span class="board-path">$project_dir</span> | |
| <span class="board-meta">$board_file</span> | |
| </a> | |
| EOF | |
| done | |
| cat >> site/hardware/index.html <<EOF | |
| </div> | |
| </section> | |
| </main> | |
| </body> | |
| </html> | |
| EOF | |
| - name: Upload GitHub Pages artifact | |
| uses: actions/upload-pages-artifact@v5 | |
| with: | |
| path: site | |
| deploy-pages: | |
| name: Deploy GitHub Pages | |
| runs-on: ubuntu-latest | |
| needs: build-pages-site | |
| if: ${{ always() && needs.build-pages-site.result == 'success' && github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch') }} | |
| permissions: | |
| pages: write | |
| id-token: write | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| outputs: | |
| page_url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Deploy GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v5 | |
| hardware-summary: | |
| name: Hardware links | |
| runs-on: ubuntu-latest | |
| needs: | |
| - project-matrix | |
| - hardware-checks | |
| - hardware-outputs | |
| - hardware-3d-model | |
| - deploy-pages | |
| if: ${{ always() }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Download ERC and DRC report artifacts | |
| uses: actions/download-artifact@v8 | |
| continue-on-error: true | |
| with: | |
| pattern: "*-check-reports" | |
| path: check-artifacts | |
| - name: Add links to run summary | |
| shell: bash | |
| env: | |
| PROJECTS_JSON: ${{ needs.project-matrix.outputs.projects }} | |
| CHECKS_RESULT: ${{ needs.hardware-checks.result }} | |
| OUTPUTS_RESULT: ${{ needs.hardware-outputs.result }} | |
| MODELS_RESULT: ${{ needs.hardware-3d-model.result }} | |
| PAGES_URL: ${{ needs.deploy-pages.outputs.page_url }} | |
| run: | | |
| set -euo pipefail | |
| run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" | |
| artifacts_url="$run_url#artifacts" | |
| pages_base="${PAGES_URL%/}" | |
| hardware_pages_url="" | |
| if [ -n "${PAGES_URL:-}" ]; then | |
| hardware_pages_url="${pages_base}/hardware/" | |
| fi | |
| { | |
| echo '# Hardware CI Summary' | |
| echo | |
| echo "| Item | Result |" | |
| echo "| --- | --- |" | |
| echo "| ERC and DRC | \`${CHECKS_RESULT}\` |" | |
| echo "| Documentation and fabrication outputs | \`${OUTPUTS_RESULT}\` |" | |
| echo "| 3D model generation | \`${MODELS_RESULT}\` |" | |
| echo | |
| echo "Run: [${GITHUB_RUN_ID}](${run_url})" | |
| if [ -n "${hardware_pages_url:-}" ]; then | |
| echo | |
| echo "Published hardware review site: [${hardware_pages_url}](${hardware_pages_url})" | |
| fi | |
| echo | |
| echo '> GitHub Actions artifacts are downloaded as archives. The review site links directly to deployed files after Pages is published.' | |
| echo | |
| echo '## Projects' | |
| echo | |
| echo '| Project | Review site | Artifacts |' | |
| echo '| --- | --- | --- |' | |
| echo "$PROJECTS_JSON" | jq -c '.[]' | while read -r project; do | |
| slug="$(echo "$project" | jq -r '.slug')" | |
| name="$(echo "$project" | jq -r '.name')" | |
| if [ -n "${hardware_pages_url:-}" ]; then | |
| page_link="[Open](${hardware_pages_url}projects/${slug}/)" | |
| else | |
| page_link="Pages not published" | |
| fi | |
| printf '| %s | %s | [%s reports, outputs, 3D](%s) |\n' "$name" "$page_link" "$slug" "$artifacts_url" | |
| done | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Add ERC and DRC issues to run summary | |
| if: ${{ always() }} | |
| shell: bash | |
| env: | |
| PROJECTS_JSON: ${{ needs.project-matrix.outputs.projects }} | |
| run: | | |
| set -euo pipefail | |
| python3 .github/hardware/report_summary.py \ | |
| --projects-json "$PROJECTS_JSON" \ | |
| --artifacts-dir check-artifacts >> "$GITHUB_STEP_SUMMARY" | |
| hardware-gate: | |
| name: Hardware CI gate | |
| runs-on: ubuntu-latest | |
| needs: | |
| - hardware-checks | |
| - hardware-outputs | |
| - build-pages-site | |
| - deploy-pages | |
| - hardware-summary | |
| if: ${{ always() }} | |
| steps: | |
| - name: Fail if validation or output generation failed | |
| shell: bash | |
| env: | |
| CHECKS_RESULT: ${{ needs.hardware-checks.result }} | |
| OUTPUTS_RESULT: ${{ needs.hardware-outputs.result }} | |
| PAGES_BUILD_RESULT: ${{ needs.build-pages-site.result }} | |
| PAGES_RESULT: ${{ needs.deploy-pages.result }} | |
| run: | | |
| set -euo pipefail | |
| failed=0 | |
| if [ "${CHECKS_RESULT:-failure}" != "success" ]; then | |
| echo "ERC/DRC failed, was skipped, or did not complete: ${CHECKS_RESULT:-missing}" | |
| failed=1 | |
| fi | |
| if [ "${OUTPUTS_RESULT:-failure}" != "success" ]; then | |
| echo "Hardware output generation failed, was skipped, or did not complete: ${OUTPUTS_RESULT:-missing}" | |
| failed=1 | |
| fi | |
| if [ "${PAGES_BUILD_RESULT:-failure}" != "success" ]; then | |
| echo "GitHub Pages site build failed or did not complete: ${PAGES_BUILD_RESULT:-missing}" | |
| failed=1 | |
| fi | |
| if [ "${GITHUB_EVENT_NAME}" != "pull_request" ] && { [ "${GITHUB_REF}" = "refs/heads/main" ] || [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; } && [ "${PAGES_RESULT}" != "success" ]; then | |
| echo "GitHub Pages deployment failed or did not complete: ${PAGES_RESULT}" | |
| failed=1 | |
| fi | |
| exit "$failed" |