Switch exercise DB to gfauredev fork, add immutable defaults with Clone & Edit, periodic refresh, and Wake Lock #28
Workflow file for this run
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: CI | |
| on: | |
| pull_request: | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| jobs: | |
| format: | |
| name: Check Formatting (cargo fmt) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt | |
| - uses: swatinem/rust-cache@v2 | |
| - name: Check formatting | |
| run: cargo fmt --check | |
| clippy: | |
| name: Lint (cargo clippy) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: clippy | |
| - uses: swatinem/rust-cache@v2 | |
| - name: Run Clippy (deny warnings) | |
| run: cargo clippy -- -D warnings | |
| unit-tests: | |
| name: Unit Tests & Coverage | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: llvm-tools-preview | |
| - uses: swatinem/rust-cache@v2 | |
| - uses: taiki-e/install-action@cargo-llvm-cov | |
| - name: Run unit tests with coverage | |
| id: tests | |
| run: | | |
| # Run tests with coverage, capturing both test output and coverage data | |
| TEST_OUTPUT=$(cargo llvm-cov --bin log-workout --lcov --output-path lcov.info 2>&1) | |
| echo "$TEST_OUTPUT" | |
| echo "test_output<<EOF" >> "$GITHUB_OUTPUT" | |
| echo "$TEST_OUTPUT" >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| - name: Generate coverage summary | |
| id: cov | |
| run: | | |
| SUMMARY=$(cargo llvm-cov report --summary-only 2>&1) | |
| echo "summary<<EOF" >> "$GITHUB_OUTPUT" | |
| echo "$SUMMARY" >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| - name: Check coverage gate for tested files | |
| id: gate | |
| run: | | |
| # Files that contain unit tests (have #[cfg(test)] blocks) | |
| TESTED_FILES=$(grep -rl '#\[cfg(test)\]' src/ | sort) | |
| echo "Tested files:" | |
| echo "$TESTED_FILES" | |
| FAIL=0 | |
| GATE_REPORT="" | |
| for f in $TESTED_FILES; do | |
| # Extract line coverage for this file from lcov.info | |
| IN_FILE=0 | |
| LH=0 | |
| LF=0 | |
| while IFS= read -r line; do | |
| case "$line" in | |
| SF:*"$f") IN_FILE=1 ;; | |
| SF:*) IN_FILE=0 ;; | |
| LH:*) [ "$IN_FILE" = 1 ] && LH="${line#LH:}" ;; | |
| LF:*) [ "$IN_FILE" = 1 ] && LF="${line#LF:}" ;; | |
| esac | |
| done < lcov.info | |
| if [ "$LF" -gt 0 ]; then | |
| PCT=$((LH * 100 / LF)) | |
| else | |
| PCT=0 | |
| fi | |
| STATUS="✅" | |
| if [ "$PCT" -lt 90 ]; then | |
| STATUS="❌" | |
| FAIL=1 | |
| fi | |
| GATE_REPORT="${GATE_REPORT}| ${f} | ${LH}/${LF} | ${PCT}% | ${STATUS} |\n" | |
| echo "$f: $LH/$LF lines covered ($PCT%)" | |
| done | |
| echo "gate_report<<EOF" >> "$GITHUB_OUTPUT" | |
| echo -e "$GATE_REPORT" >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| echo "gate_failed=$FAIL" >> "$GITHUB_OUTPUT" | |
| - name: Post unit-test coverage comment | |
| uses: actions/github-script@v7 | |
| env: | |
| TEST_OUTPUT: ${{ steps.tests.outputs.test_output }} | |
| SUMMARY: ${{ steps.cov.outputs.summary }} | |
| GATE_REPORT: ${{ steps.gate.outputs.gate_report }} | |
| GATE_FAILED: ${{ steps.gate.outputs.gate_failed }} | |
| with: | |
| script: | | |
| // Access variables via process.env | |
| const testOutput = process.env.TEST_OUTPUT; | |
| const summary = process.env.SUMMARY; | |
| const gateReport = process.env.GATE_REPORT; | |
| const gateFailed = process.env.GATE_FAILED === '1'; | |
| const gateStatus = gateFailed | |
| ? '❌ **FAILED** — tested files must have ≥ 90% coverage' | |
| : '✅ **PASSED** — all tested files have ≥ 90% coverage'; | |
| const tag = ''; | |
| const body = `${tag} | |
| ## 🧪 Unit Tests & Coverage | |
| ### Test Results | |
| \`\`\` | |
| ${testOutput} | |
| \`\`\` | |
| ### Coverage Summary | |
| \`\`\` | |
| ${summary} | |
| \`\`\` | |
| ### Coverage Gate ${gateStatus} | |
| | File | Lines Hit/Total | Coverage | Status | | |
| |------|----------------|----------|--------| | |
| ${gateReport}`; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find(c => c.body.includes(tag)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| } | |
| - name: Upload lcov report | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: unit-coverage-lcov | |
| path: lcov.info | |
| retention-days: 7 | |
| - name: Fail if coverage gate not met | |
| if: steps.gate.outputs.gate_failed == '1' | |
| run: | | |
| echo "::error::Coverage gate failed: one or more tested files have less than 90% line coverage." | |
| exit 1 | |
| e2e: | |
| name: E2E Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| target: wasm32-unknown-unknown | |
| - uses: swatinem/rust-cache@v2 | |
| - name: Install Dioxus CLI | |
| uses: taiki-e/install-action@v2 | |
| with: | |
| tool: dioxus-cli | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| - run: npm ci | |
| - name: Install Playwright browsers | |
| run: npx playwright install --with-deps chromium | |
| - name: Run E2E tests | |
| id: e2e | |
| run: npx playwright test | |
| continue-on-error: true | |
| - name: Upload screenshots on failure | |
| if: steps.e2e.outcome == 'failure' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: e2e-screenshots | |
| path: test-results/ | |
| if-no-files-found: ignore | |
| - name: Post failure-screenshots comment | |
| if: steps.e2e.outcome == 'failure' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const tag = ''; | |
| const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| let body = `${tag}\n## 📸 E2E Test Failures\n\n`; | |
| body += `Test failures were detected. You can view and download the full screenshots in the **Artifacts** section of the [Action Run](${runUrl}).\n\n`; | |
| const testResultsDir = 'test-results'; | |
| if (fs.existsSync(testResultsDir)) { | |
| const walk = (dir) => { | |
| let files = []; | |
| for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { | |
| const full = path.join(dir, entry.name); | |
| if (entry.isDirectory()) files.push(...walk(full)); | |
| else if (entry.name.endsWith('.png')) files.push(entry.name); | |
| } | |
| return files; | |
| }; | |
| const pngNames = walk(testResultsDir); | |
| if (pngNames.length > 0) { | |
| body += `### Failed Screenshots Found:\n`; | |
| pngNames.forEach(name => { | |
| body += `- \`${name}\` \n`; | |
| }); | |
| } else { | |
| body += '> [!NOTE]\n> No .png files found in the test-results directory.\n'; | |
| } | |
| } else { | |
| body += '> [!WARNING]\n> The `test-results` directory was not found.\n'; | |
| } | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find(c => c.body.includes(tag)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| } | |
| - name: Fail if any E2E test failed | |
| if: steps.e2e.outcome == 'failure' | |
| run: exit 1 | |
| lighthouse: | |
| name: PageSpeed Insights (Lighthouse) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| target: wasm32-unknown-unknown | |
| - uses: swatinem/rust-cache@v2 | |
| - name: Install Dioxus CLI | |
| uses: taiki-e/install-action@v2 | |
| with: | |
| tool: dioxus-cli | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| - name: Install Lighthouse CLI | |
| run: npm install -g lighthouse | |
| - name: Start dev server | |
| run: dx serve --port 3000 & | |
| - name: Wait for server | |
| run: | | |
| echo "Waiting for server to be ready..." | |
| for i in $(seq 1 120); do | |
| if curl -sf http://localhost:3000/LogOut/ > /dev/null 2>&1; then | |
| echo "Server is ready." | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| - name: Run Lighthouse | |
| id: lighthouse | |
| run: | | |
| lighthouse http://localhost:3000/LogOut/ \ | |
| --output json html \ | |
| --output-path ./lighthouse-report \ | |
| --chrome-flags="--headless --no-sandbox --disable-dev-shm-usage" \ | |
| --quiet || true | |
| if [ -f ./lighthouse-report.report.json ]; then | |
| PERF=$(jq '.categories.performance.score * 100 | round' ./lighthouse-report.report.json) | |
| A11Y=$(jq '.categories.accessibility.score * 100 | round' ./lighthouse-report.report.json) | |
| BP=$(jq '(.categories["best-practices"].score // 0) * 100 | round' ./lighthouse-report.report.json) | |
| SEO=$(jq '.categories.seo.score * 100 | round' ./lighthouse-report.report.json) | |
| PWA=$(jq '(.categories.pwa.score // 0) * 100 | round' ./lighthouse-report.report.json) | |
| echo "performance=$PERF" >> "$GITHUB_OUTPUT" | |
| echo "accessibility=$A11Y" >> "$GITHUB_OUTPUT" | |
| echo "best_practices=$BP" >> "$GITHUB_OUTPUT" | |
| echo "seo=$SEO" >> "$GITHUB_OUTPUT" | |
| echo "pwa=$PWA" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Upload Lighthouse HTML report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: lighthouse-report | |
| path: ./lighthouse-report.report.html | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| - name: Post Lighthouse results comment | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const score = s => { | |
| const n = parseInt(s) || 0; | |
| return n >= 90 ? `🟢 ${n}` : n >= 50 ? `🟡 ${n}` : `🔴 ${n}`; | |
| }; | |
| const perf = '${{ steps.lighthouse.outputs.performance }}'; | |
| const a11y = '${{ steps.lighthouse.outputs.accessibility }}'; | |
| const bp = '${{ steps.lighthouse.outputs.best_practices }}'; | |
| const seo = '${{ steps.lighthouse.outputs.seo }}'; | |
| const pwa = '${{ steps.lighthouse.outputs.pwa }}'; | |
| const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const tag = '<!-- lighthouse-report -->'; | |
| const body = `${tag} | |
| ## 🚀 PageSpeed Insights (Lighthouse) | |
| | Category | Score | | |
| |----------|-------| | |
| | ⚡ Performance | ${score(perf)} | | |
| | ♿ Accessibility | ${score(a11y)} | | |
| | 🛡️ Best Practices | ${score(bp)} | | |
| | 🔍 SEO | ${score(seo)} | | |
| | 📱 PWA | ${score(pwa)} | | |
| > 🟢 ≥ 90 · 🟡 50–89 · 🔴 < 50 | |
| [📄 Full Lighthouse HTML report](${runUrl})`; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find(c => c.body.includes(tag)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| } | |
| preview: | |
| name: PR Preview Build | |
| runs-on: ubuntu-latest | |
| needs: [format, clippy, unit-tests, e2e] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| target: wasm32-unknown-unknown | |
| - uses: swatinem/rust-cache@v2 | |
| - name: Install Dioxus CLI | |
| uses: taiki-e/install-action@v2 | |
| with: | |
| tool: dioxus-cli | |
| - name: Build for preview | |
| run: dx build --release --platform web | |
| - name: Upload preview build | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pr-preview-build | |
| path: ./target/dx/log-workout/release/web/public | |
| retention-days: 14 | |
| - name: Post preview comment | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const tag = '<!-- pr-preview -->'; | |
| const body = `${tag}\n## 🔍 PR Preview Build\nA preview build of this PR is available as a workflow artifact.\n👉 [Download preview build](${runUrl})\n\n> To view locally, download and serve the artifact with any static file server.`; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find(c => c.body.includes(tag)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| } |