Skip to content

Switch exercise DB to gfauredev fork, add immutable defaults with Clone & Edit, periodic refresh, and Wake Lock #28

Switch exercise DB to gfauredev fork, add immutable defaults with Clone & Edit, periodic refresh, and Wake Lock

Switch exercise DB to gfauredev fork, add immutable defaults with Clone & Edit, periodic refresh, and Wake Lock #28

Workflow file for this run

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,
});
}