feat: publish exchange rates to Nostr #156
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: Mutation Testing | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| types: [opened, synchronize, reopened, labeled] | |
| workflow_dispatch: | |
| schedule: | |
| # Run full mutation testing weekly on Sundays at 3 AM UTC | |
| - cron: '0 3 * * 0' | |
| env: | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| # Quick mutation testing on PRs (only changed files) | |
| # Opt-in only: add `run-mutation` label to the PR when needed. | |
| mutation-pr: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-mutation') | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Need full history for --in-diff | |
| - uses: dtolnay/rust-toolchain@stable | |
| - name: Install protobuf | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y protobuf-compiler | |
| - name: Cache cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-on-failure: true | |
| - name: Install cargo-mutants | |
| run: cargo install cargo-mutants | |
| - name: Run mutation testing on changed files | |
| # Only test mutants in files changed in this PR | |
| # Runs only when PR has the `run-mutation` label | |
| continue-on-error: true | |
| run: | | |
| # Ensure base branch ref exists locally for reliable diffing | |
| git fetch origin "${{ github.base_ref }}":"refs/remotes/origin/${{ github.base_ref }}" | |
| # Collect changed Rust source files (exclude test-only and workflow files) | |
| changed_rs=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- '*.rs' | grep -v '_test\.rs$' || true) | |
| if [ -z "$changed_rs" ]; then | |
| echo "No Rust source files changed in this PR" | |
| exit 0 | |
| fi | |
| # Build --file flags for each changed file | |
| file_args="" | |
| for f in $changed_rs; do | |
| file_args="$file_args --file $f" | |
| done | |
| echo "Running mutation testing for changed files:" | |
| echo "$changed_rs" | |
| cargo mutants $file_args | |
| - name: Upload mutation report | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: mutation-report-pr | |
| path: mutants.out/ | |
| retention-days: 30 | |
| - name: Comment PR with mutation results | |
| if: always() && github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| // Try the expected path first | |
| let outcomesPath = path.join('mutants.out', 'outcomes.json'); | |
| // If not found, search under the workspace so we can handle any | |
| // unexpected layout changes while still using the results. | |
| if (!fs.existsSync(outcomesPath)) { | |
| const workspace = process.env.GITHUB_WORKSPACE || process.cwd(); | |
| /** @param {string} start */ | |
| function findOutcomes(start) { | |
| const entries = fs.readdirSync(start, { withFileTypes: true }); | |
| for (const entry of entries) { | |
| const full = path.join(start, entry.name); | |
| if (entry.isDirectory()) { | |
| const found = findOutcomes(full); | |
| if (found) return found; | |
| } else if (entry.isFile() && entry.name === 'outcomes.json') { | |
| return full; | |
| } | |
| } | |
| return null; | |
| } | |
| const found = fs.existsSync(workspace) ? findOutcomes(workspace) : null; | |
| if (found) { | |
| console.log(`Found outcomes.json at ${found}`); | |
| outcomesPath = found; | |
| } else { | |
| console.log('No mutation outcomes file found (no outcomes.json anywhere under workspace)'); | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: '## 🧬 Mutation Testing Results\n\nNo mutation outcomes file was found for this run. This usually means no mutants were generated for the changed files or mutation testing did not complete.' | |
| }); | |
| return; | |
| } | |
| } | |
| const data = JSON.parse(fs.readFileSync(outcomesPath, 'utf8')); | |
| const outcomes = data.outcomes || []; | |
| const mutants = outcomes.filter(o => o.scenario !== 'Baseline'); | |
| const total = mutants.length; | |
| const killed = mutants.filter(o => o.summary === 'CaughtMutant').length; | |
| const survived = mutants.filter(o => o.summary === 'MissedMutant').length; | |
| const timeout = mutants.filter(o => o.summary === 'Timeout').length; | |
| const score = total > 0 ? ((killed / total) * 100).toFixed(1) : 0; | |
| const survivorList = mutants | |
| .filter(o => o.summary === 'MissedMutant') | |
| .map(o => { | |
| const name = o.scenario?.Mutant?.name || 'unknown'; | |
| return `- \`${name}\``; | |
| }) | |
| .join('\n') || 'None'; | |
| const body = [ | |
| '## 🧬 Mutation Testing Results', | |
| '', | |
| '| Metric | Count |', | |
| '|--------|-------|', | |
| `| Total Mutants | ${total} |`, | |
| `| Killed | ${killed} |`, | |
| `| Survived | ${survived} |`, | |
| `| Timeout | ${timeout} |`, | |
| `| **Score** | **${score}%** |`, | |
| '', | |
| survived > 0 ? '**Note:** Some mutants survived. Consider improving tests for the changed code.' : 'All mutants killed!', | |
| '', | |
| '<details>', | |
| '<summary>Surviving Mutants</summary>', | |
| '', | |
| survivorList, | |
| '', | |
| '</details>' | |
| ].join('\n'); | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| # Full mutation testing on main branch (baseline) | |
| mutation-baseline: | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/main' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - name: Install protobuf | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y protobuf-compiler | |
| - name: Cache cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-on-failure: true | |
| - name: Install cargo-mutants | |
| run: cargo install cargo-mutants | |
| - name: Run full mutation testing | |
| run: cargo mutants | |
| # Note: Do NOT fail on low score initially (report only mode) | |
| continue-on-error: true | |
| - name: Upload mutation report | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: mutation-report-baseline | |
| path: | | |
| mutants.out/ | |
| retention-days: 90 | |
| - name: Upload HTML report to GitHub Pages | |
| if: github.ref == 'refs/heads/main' && always() | |
| uses: peaceiris/actions-gh-pages@v4 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| publish_dir: ./mutants.out/html | |
| destination_dir: mutation-testing | |
| - name: Generate mutation summary | |
| if: always() | |
| run: | | |
| echo "## Mutation Testing Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ -f mutants.out/outcomes.json ]; then | |
| total=$(jq '[.outcomes[] | select(.scenario != "Baseline")] | length' mutants.out/outcomes.json) | |
| killed=$(jq '[.outcomes[] | select(.scenario != "Baseline") | select(.summary == "CaughtMutant")] | length' mutants.out/outcomes.json) | |
| survived=$(jq '[.outcomes[] | select(.scenario != "Baseline") | select(.summary == "MissedMutant")] | length' mutants.out/outcomes.json) | |
| timeout=$(jq '[.outcomes[] | select(.scenario != "Baseline") | select(.summary == "Timeout")] | length' mutants.out/outcomes.json) | |
| unviable=$(jq '[.outcomes[] | select(.scenario != "Baseline") | select(.summary == "Unviable")] | length' mutants.out/outcomes.json) | |
| if [ "$total" -gt 0 ]; then | |
| score=$(echo "scale=1; ($killed / $total) * 100" | bc) | |
| echo "| Metric | Count |" >> $GITHUB_STEP_SUMMARY | |
| echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Total Mutants | $total |" >> $GITHUB_STEP_SUMMARY | |
| echo "| ✅ Killed | $killed |" >> $GITHUB_STEP_SUMMARY | |
| echo "| ❌ Survived | $survived |" >> $GITHUB_STEP_SUMMARY | |
| echo "| ⏱️ Timeout | $timeout |" >> $GITHUB_STEP_SUMMARY | |
| echo "| ⚪ Unviable | $unviable |" >> $GITHUB_STEP_SUMMARY | |
| echo "| **Mutation Score** | **$score%** |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if (( $(echo "$score >= 80" | bc -l) )); then | |
| echo "✅ **Excellent test quality!**" >> $GITHUB_STEP_SUMMARY | |
| elif (( $(echo "$score >= 50" | bc -l) )); then | |
| echo "⚠️ **Acceptable test quality, room for improvement.**" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "🔴 **Poor test quality. Consider improving tests.**" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| fi | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "📊 [View Full Report](https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/mutation-testing/)" >> $GITHUB_STEP_SUMMARY |