From bcb69411391a8d4eaa1175940df2adedfa0a9a34 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Thu, 23 Oct 2025 21:28:25 -0400 Subject: [PATCH 01/20] add coverage ci --- .github/workflows/coverage.yml | 72 ++++++++++++++++++++++++++++++++++ foundry.toml | 10 +++++ 2 files changed, 82 insertions(+) create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..59e6f6a1 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,72 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + name: Coverage report + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Install coverage tools + run: | + sudo apt-get update + sudo apt-get install -y lcov + + - name: Run tests with coverage + run: | + forge coverage --report lcov + + - name: Generate coverage report + run: | + lcov --capture --directory . --output-file coverage.info + lcov --remove coverage.info '*/test/*' '*/script/*' '*/lib/*' --output-file coverage.info + lcov --list coverage.info + + - name: Comment coverage on PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const lcov = fs.readFileSync('coverage.info', 'utf8'); + const lines = lcov.split('\n'); + const summary = lines.find(line => line.startsWith('LF:')); + const hit = lines.find(line => line.startsWith('LH:')); + + if (summary && hit) { + const total = summary.split(':')[1]; + const covered = hit.split(':')[1]; + const percentage = Math.round((covered / total) * 100); + + const comment = `## Test Coverage Report + + **Coverage: ${percentage}%** (${covered}/${total} lines) + +
+ View detailed coverage report + + \`\`\` + ${lcov} + \`\`\` +
`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } diff --git a/foundry.toml b/foundry.toml index da075af4..4e610efe 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,4 +5,14 @@ libs = ["lib"] optimizer = true optimizer_runs = 20_000 +[profile.ci] +src = "src" +out = "out" +libs = ["lib"] +optimizer = true +optimizer_runs = 20_000 +# Coverage settings +fuzz = { runs = 1000 } +invariant = { runs = 1000 } + # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options From 13fc65b501cb70d6b2b731a165ef5eaa97fd0f95 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Thu, 23 Oct 2025 21:28:51 -0400 Subject: [PATCH 02/20] add slither ci --- .github/workflows/slither.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/slither.yml diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml new file mode 100644 index 00000000..f2e32020 --- /dev/null +++ b/.github/workflows/slither.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + name: Static Analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Build contracts + run: forge build + + - name: Install Slither + run: | + pip install slither-analyzer + + - name: Run Slither analysis + run: | + slither . --exclude-dependencies \ No newline at end of file From a786cb7554bf87d1cb03a8ad523089429f1d0bac Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Thu, 23 Oct 2025 21:31:33 -0400 Subject: [PATCH 03/20] fix coverage --- .github/workflows/coverage.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 59e6f6a1..de1502a7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -27,13 +27,17 @@ jobs: - name: Run tests with coverage run: | - forge coverage --report lcov + forge coverage --report lcov --report-file coverage.lcov - - name: Generate coverage report + - name: Process coverage report run: | - lcov --capture --directory . --output-file coverage.info - lcov --remove coverage.info '*/test/*' '*/script/*' '*/lib/*' --output-file coverage.info - lcov --list coverage.info + if [ -f coverage.lcov ]; then + lcov --remove coverage.lcov '*/test/*' '*/script/*' '*/lib/*' --output-file coverage.info + lcov --list coverage.info + else + echo "No coverage file found" + exit 1 + fi - name: Comment coverage on PR if: github.event_name == 'pull_request' From 6ce60cfcde2c989f2bf1d42df3d7f531470d1f93 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Thu, 23 Oct 2025 21:34:22 -0400 Subject: [PATCH 04/20] fix coverage ci again --- .github/workflows/coverage.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index de1502a7..3c7dc8a1 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -20,24 +20,28 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 + - name: Run tests with coverage + run: | + forge coverage --report lcov + - name: Install coverage tools run: | sudo apt-get update sudo apt-get install -y lcov - - name: Run tests with coverage - run: | - forge coverage --report lcov --report-file coverage.lcov - - name: Process coverage report run: | - if [ -f coverage.lcov ]; then - lcov --remove coverage.lcov '*/test/*' '*/script/*' '*/lib/*' --output-file coverage.info - lcov --list coverage.info + # Find the coverage file that Foundry generated + if [ -f lcov.info ]; then + echo "Found lcov.info, processing..." + lcov --remove lcov.info '*/test/*' '*/script/*' '*/lib/*' --output-file coverage.info else - echo "No coverage file found" + echo "No lcov.info found, looking for other coverage files..." + ls -la *.lcov || echo "No .lcov files found" exit 1 fi + + lcov --list coverage.info - name: Comment coverage on PR if: github.event_name == 'pull_request' From a7e3cab290a376743022b5d665496251cfc4b77b Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 09:44:36 -0400 Subject: [PATCH 05/20] rework on coverage --- .github/workflows/coverage.yml | 73 ++++++++++------------------------ 1 file changed, 22 insertions(+), 51 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 3c7dc8a1..8b3fb8f5 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,16 +1,14 @@ name: CI on: - push: pull_request: - workflow_dispatch: env: FOUNDRY_PROFILE: ci jobs: check: - name: Coverage report + name: Coverage runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -20,61 +18,34 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 - - name: Run tests with coverage + - name: Run coverage run: | - forge coverage --report lcov + forge coverage --report summary --report lcov + ls -la lcov.info || echo "lcov.info not found" - - name: Install coverage tools - run: | - sudo apt-get update - sudo apt-get install -y lcov - - - name: Process coverage report - run: | - # Find the coverage file that Foundry generated - if [ -f lcov.info ]; then - echo "Found lcov.info, processing..." - lcov --remove lcov.info '*/test/*' '*/script/*' '*/lib/*' --output-file coverage.info - else - echo "No lcov.info found, looking for other coverage files..." - ls -la *.lcov || echo "No .lcov files found" - exit 1 - fi - - lcov --list coverage.info - - - name: Comment coverage on PR - if: github.event_name == 'pull_request' + - name: Comment on PR uses: actions/github-script@v7 with: + github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); - const lcov = fs.readFileSync('coverage.info', 'utf8'); - const lines = lcov.split('\n'); - const summary = lines.find(line => line.startsWith('LF:')); - const hit = lines.find(line => line.startsWith('LH:')); - - if (summary && hit) { - const total = summary.split(':')[1]; - const covered = hit.split(':')[1]; - const percentage = Math.round((covered / total) * 100); - - const comment = `## Test Coverage Report - - **Coverage: ${percentage}%** (${covered}/${total} lines) - -
- View detailed coverage report - - \`\`\` - ${lcov} - \`\`\` -
`; - - github.rest.issues.createComment({ - issue_number: context.issue.number, + const file = 'lcov.info'; + let pct = '-'; + if (fs.existsSync(file)) { + const content = fs.readFileSync(file, 'utf8'); + const match = content.match(/LF:\s*(\d+)\s*LH:\s*(\d+)/); + if (match) { + const total = Number(match[1]); + const covered = Number(match[2]); + pct = Math.round((covered / total)*100) + '%'; + } + const body = `## 📊 Coverage Report\n\n**Coverage:** ${pct}\n\n
lcov.info\n\`\`\`\n${fs.readFileSync(file,'utf8')}\n\`\`\`\n
`; + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, - body: comment + issue_number: context.issue.number, + body: body }); + } else { + console.log('Coverage file not found.'); } From 44146b307acaac7153cda95d6bb867f591a6eeaa Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 09:50:32 -0400 Subject: [PATCH 06/20] adjust coverage actions to comment only on PR --- .github/workflows/coverage.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8b3fb8f5..a17bd03e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,7 +1,9 @@ name: CI on: + push: pull_request: + workflow_dispatch: env: FOUNDRY_PROFILE: ci @@ -24,6 +26,7 @@ jobs: ls -la lcov.info || echo "lcov.info not found" - name: Comment on PR + if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -39,7 +42,7 @@ jobs: const covered = Number(match[2]); pct = Math.round((covered / total)*100) + '%'; } - const body = `## 📊 Coverage Report\n\n**Coverage:** ${pct}\n\n
lcov.info\n\`\`\`\n${fs.readFileSync(file,'utf8')}\n\`\`\`\n
`; + const body = `## Coverage Report\n\n**Coverage:** ${pct}\n\n
lcov.info\n\`\`\`\n${fs.readFileSync(file,'utf8')}\n\`\`\`\n
`; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, From 06660bcbb0df8821deecbba549c260566aae6db7 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 10:01:11 -0400 Subject: [PATCH 07/20] update coverage report --- .github/workflows/coverage.yml | 66 +++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a17bd03e..cf40260a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -2,7 +2,10 @@ name: CI on: push: + branches: [main] pull_request: + paths-ignore: + - '**.md' workflow_dispatch: env: @@ -42,7 +45,68 @@ jobs: const covered = Number(match[2]); pct = Math.round((covered / total)*100) + '%'; } - const body = `## Coverage Report\n\n**Coverage:** ${pct}\n\n
lcov.info\n\`\`\`\n${fs.readFileSync(file,'utf8')}\n\`\`\`\n
`; + // THIS IS THE THRESHOLDS + const goodThreshold = 80; + const needsImprovementThreshold = 60; + const poorThreshold = 40; + + // Parse more detailed coverage info + const lines = content.split('\n'); + let totalLines = 0; + let coveredLines = 0; + let totalFunctions = 0; + let coveredFunctions = 0; + let totalBranches = 0; + let coveredBranches = 0; + + lines.forEach(line => { + if (line.startsWith('LF:')) { + const match = line.match(/LF:\s*(\d+)\s*LH:\s*(\d+)/); + if (match) { + totalLines = parseInt(match[1]); + coveredLines = parseInt(match[2]); + } + } + if (line.startsWith('FNF:')) { + const match = line.match(/FNF:\s*(\d+)\s*FNH:\s*(\d+)/); + if (match) { + totalFunctions = parseInt(match[1]); + coveredFunctions = parseInt(match[2]); + } + } + if (line.startsWith('BRF:')) { + const match = line.match(/BRF:\s*(\d+)\s*BRH:\s*(\d+)/); + if (match) { + totalBranches = parseInt(match[1]); + coveredBranches = parseInt(match[2]); + } + } + }); + + const lineCoverage = totalLines > 0 ? Math.round((coveredLines / totalLines) * 100) : 0; + const functionCoverage = totalFunctions > 0 ? Math.round((coveredFunctions / totalFunctions) * 100) : 0; + const branchCoverage = totalBranches > 0 ? Math.round((coveredBranches / totalBranches) * 100) : 0; + + // Create coverage badge + const getBadgeColor = (coverage) => { + if (coverage >= 80) return 'brightgreen'; + if (coverage >= 60) return 'yellow'; + if (coverage >= 40) return 'orange'; + return 'red'; + }; + + const badgeColor = getBadgeColor(lineCoverage); + const badge = `![Coverage](https://img.shields.io/badge/coverage-${lineCoverage}%25-${badgeColor})`; + + const body = `## Coverage Report ${badge}\n\n` + + `| Metric | Coverage | Details |\n` + + `|--------|----------|----------|\n` + + `| **Lines** | ${lineCoverage}% | ${coveredLines}/${totalLines} lines |\n` + + `| **Functions** | ${functionCoverage}% | ${coveredFunctions}/${totalFunctions} functions |\n` + + `| **Branches** | ${branchCoverage}% | ${coveredBranches}/${totalBranches} branches |\n\n` + + `### Coverage Summary\n` + + `- **Overall Coverage:** ${pct}\n` + + `- **Status:** ${lineCoverage >= 80 ? '✅ Good' : lineCoverage >= 60 ? '⚠️ Needs Improvement' : '❌ Poor'}\n\n` + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, From 4f02ccee204edf61d62aebb5ba5c9cc2a249477a Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 10:01:37 -0400 Subject: [PATCH 08/20] update again --- .github/workflows/coverage.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index cf40260a..4e8ec203 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -89,9 +89,9 @@ jobs: // Create coverage badge const getBadgeColor = (coverage) => { - if (coverage >= 80) return 'brightgreen'; - if (coverage >= 60) return 'yellow'; - if (coverage >= 40) return 'orange'; + if (coverage >= goodThreshold) return 'brightgreen'; + if (coverage >= needsImprovementThreshold) return 'yellow'; + if (coverage >= poorThreshold) return 'orange'; return 'red'; }; @@ -106,7 +106,7 @@ jobs: `| **Branches** | ${branchCoverage}% | ${coveredBranches}/${totalBranches} branches |\n\n` + `### Coverage Summary\n` + `- **Overall Coverage:** ${pct}\n` + - `- **Status:** ${lineCoverage >= 80 ? '✅ Good' : lineCoverage >= 60 ? '⚠️ Needs Improvement' : '❌ Poor'}\n\n` + + `- **Status:** ${lineCoverage >= goodThreshold ? '✅ Good' : lineCoverage >= needsImprovementThreshold ? '⚠️ Needs Improvement' : '❌ Poor'}\n\n` + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, From bf628c316c20395c3a795c2093bdd55d4a962e7a Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 15:19:46 -0400 Subject: [PATCH 09/20] update coverage action for better output --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 4e8ec203..1e8daa33 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -106,7 +106,7 @@ jobs: `| **Branches** | ${branchCoverage}% | ${coveredBranches}/${totalBranches} branches |\n\n` + `### Coverage Summary\n` + `- **Overall Coverage:** ${pct}\n` + - `- **Status:** ${lineCoverage >= goodThreshold ? '✅ Good' : lineCoverage >= needsImprovementThreshold ? '⚠️ Needs Improvement' : '❌ Poor'}\n\n` + + `- **Status:** ${lineCoverage >= goodThreshold ? '✅ Good' : lineCoverage >= needsImprovementThreshold ? '⚠️ Needs Improvement' : '❌ Poor'}`; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, From 8b42eeb4d7e05be21dbaa7b62c495556168fd765 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 15:25:21 -0400 Subject: [PATCH 10/20] get totals --- .github/workflows/coverage.yml | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 1e8daa33..f24be48c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -45,12 +45,13 @@ jobs: const covered = Number(match[2]); pct = Math.round((covered / total)*100) + '%'; } + // THIS IS THE THRESHOLDS const goodThreshold = 80; const needsImprovementThreshold = 60; const poorThreshold = 40; - // Parse more detailed coverage info + // Parse more detailed coverage info - get the LAST occurrence (summary) const lines = content.split('\n'); let totalLines = 0; let coveredLines = 0; @@ -59,29 +60,37 @@ jobs: let totalBranches = 0; let coveredBranches = 0; - lines.forEach(line => { - if (line.startsWith('LF:')) { + // Find the last occurrence of each metric (which should be the totals) + for (let i = lines.length - 1; i >= 0; i--) { + const line = lines[i]; + + if (line.startsWith('LF:') && totalLines === 0) { const match = line.match(/LF:\s*(\d+)\s*LH:\s*(\d+)/); if (match) { totalLines = parseInt(match[1]); coveredLines = parseInt(match[2]); } } - if (line.startsWith('FNF:')) { + if (line.startsWith('FNF:') && totalFunctions === 0) { const match = line.match(/FNF:\s*(\d+)\s*FNH:\s*(\d+)/); if (match) { totalFunctions = parseInt(match[1]); coveredFunctions = parseInt(match[2]); } } - if (line.startsWith('BRF:')) { + if (line.startsWith('BRF:') && totalBranches === 0) { const match = line.match(/BRF:\s*(\d+)\s*BRH:\s*(\d+)/); if (match) { totalBranches = parseInt(match[1]); coveredBranches = parseInt(match[2]); } } - }); + + // Stop if we found all metrics + if (totalLines > 0 && totalFunctions > 0 && totalBranches > 0) { + break; + } + } const lineCoverage = totalLines > 0 ? Math.round((coveredLines / totalLines) * 100) : 0; const functionCoverage = totalFunctions > 0 ? Math.round((coveredFunctions / totalFunctions) * 100) : 0; @@ -98,15 +107,13 @@ jobs: const badgeColor = getBadgeColor(lineCoverage); const badge = `![Coverage](https://img.shields.io/badge/coverage-${lineCoverage}%25-${badgeColor})`; - const body = `## Coverage Report ${badge}\n\n` + + const body = `## Coverage Report\n` + + `${badge}\n\n` + `| Metric | Coverage | Details |\n` + `|--------|----------|----------|\n` + `| **Lines** | ${lineCoverage}% | ${coveredLines}/${totalLines} lines |\n` + `| **Functions** | ${functionCoverage}% | ${coveredFunctions}/${totalFunctions} functions |\n` + - `| **Branches** | ${branchCoverage}% | ${coveredBranches}/${totalBranches} branches |\n\n` + - `### Coverage Summary\n` + - `- **Overall Coverage:** ${pct}\n` + - `- **Status:** ${lineCoverage >= goodThreshold ? '✅ Good' : lineCoverage >= needsImprovementThreshold ? '⚠️ Needs Improvement' : '❌ Poor'}`; + `| **Branches** | ${branchCoverage}% | ${coveredBranches}/${totalBranches} branches |\n\n` +; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, From 51c3d03eace74203e496777ff63fd3e8fe8ec706 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 15:26:24 -0400 Subject: [PATCH 11/20] fix syntax --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f24be48c..55a16d5b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -113,7 +113,7 @@ jobs: `|--------|----------|----------|\n` + `| **Lines** | ${lineCoverage}% | ${coveredLines}/${totalLines} lines |\n` + `| **Functions** | ${functionCoverage}% | ${coveredFunctions}/${totalFunctions} functions |\n` + - `| **Branches** | ${branchCoverage}% | ${coveredBranches}/${totalBranches} branches |\n\n` +; + `| **Branches** | ${branchCoverage}% | ${coveredBranches}/${totalBranches} branches |\n\n`; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, From d31f8cc507c396014baa2dd1411a5ad53704883c Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 15:30:46 -0400 Subject: [PATCH 12/20] test --- .github/workflows/coverage.yml | 69 ++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 55a16d5b..10018bc5 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -51,7 +51,7 @@ jobs: const needsImprovementThreshold = 60; const poorThreshold = 40; - // Parse more detailed coverage info - get the LAST occurrence (summary) + // Parse more detailed coverage info - look for summary section const lines = content.split('\n'); let totalLines = 0; let coveredLines = 0; @@ -60,35 +60,48 @@ jobs: let totalBranches = 0; let coveredBranches = 0; - // Find the last occurrence of each metric (which should be the totals) - for (let i = lines.length - 1; i >= 0; i--) { - const line = lines[i]; - - if (line.startsWith('LF:') && totalLines === 0) { - const match = line.match(/LF:\s*(\d+)\s*LH:\s*(\d+)/); - if (match) { - totalLines = parseInt(match[1]); - coveredLines = parseInt(match[2]); - } - } - if (line.startsWith('FNF:') && totalFunctions === 0) { - const match = line.match(/FNF:\s*(\d+)\s*FNH:\s*(\d+)/); - if (match) { - totalFunctions = parseInt(match[1]); - coveredFunctions = parseInt(match[2]); - } + // Debug: log the content to see what we're working with + console.log('lcov.info content length:', content.length); + console.log('First 500 chars:', content.substring(0, 500)); + console.log('Last 500 chars:', content.substring(content.length - 500)); + + // Look for all LF:, FNF:, BRF: lines and take the last ones + const lfLines = lines.filter(line => line.startsWith('LF:')); + const fnfLines = lines.filter(line => line.startsWith('FNF:')); + const brfLines = lines.filter(line => line.startsWith('BRF:')); + + console.log('Found LF lines:', lfLines.length); + console.log('Found FNF lines:', fnfLines.length); + console.log('Found BRF lines:', brfLines.length); + + // Get the last occurrence of each (should be the totals) + if (lfLines.length > 0) { + const lastLF = lfLines[lfLines.length - 1]; + const match = lastLF.match(/LF:\s*(\d+)\s*LH:\s*(\d+)/); + if (match) { + totalLines = parseInt(match[1]); + coveredLines = parseInt(match[2]); + console.log('Total lines:', totalLines, 'Covered lines:', coveredLines); } - if (line.startsWith('BRF:') && totalBranches === 0) { - const match = line.match(/BRF:\s*(\d+)\s*BRH:\s*(\d+)/); - if (match) { - totalBranches = parseInt(match[1]); - coveredBranches = parseInt(match[2]); - } + } + + if (fnfLines.length > 0) { + const lastFNF = fnfLines[fnfLines.length - 1]; + const match = lastFNF.match(/FNF:\s*(\d+)\s*FNH:\s*(\d+)/); + if (match) { + totalFunctions = parseInt(match[1]); + coveredFunctions = parseInt(match[2]); + console.log('Total functions:', totalFunctions, 'Covered functions:', coveredFunctions); } - - // Stop if we found all metrics - if (totalLines > 0 && totalFunctions > 0 && totalBranches > 0) { - break; + } + + if (brfLines.length > 0) { + const lastBRF = brfLines[brfLines.length - 1]; + const match = lastBRF.match(/BRF:\s*(\d+)\s*BRH:\s*(\d+)/); + if (match) { + totalBranches = parseInt(match[1]); + coveredBranches = parseInt(match[2]); + console.log('Total branches:', totalBranches, 'Covered branches:', coveredBranches); } } From 9eb749a8680a01bcc2979461c34cfea2c2f32f01 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 15:32:28 -0400 Subject: [PATCH 13/20] test 2 --- .github/workflows/coverage.yml | 68 +++++++++++++--------------------- 1 file changed, 26 insertions(+), 42 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 10018bc5..f0102519 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -51,7 +51,7 @@ jobs: const needsImprovementThreshold = 60; const poorThreshold = 40; - // Parse more detailed coverage info - look for summary section + // Parse more detailed coverage info - sum all individual file records const lines = content.split('\n'); let totalLines = 0; let coveredLines = 0; @@ -60,50 +60,34 @@ jobs: let totalBranches = 0; let coveredBranches = 0; - // Debug: log the content to see what we're working with - console.log('lcov.info content length:', content.length); - console.log('First 500 chars:', content.substring(0, 500)); - console.log('Last 500 chars:', content.substring(content.length - 500)); - - // Look for all LF:, FNF:, BRF: lines and take the last ones - const lfLines = lines.filter(line => line.startsWith('LF:')); - const fnfLines = lines.filter(line => line.startsWith('FNF:')); - const brfLines = lines.filter(line => line.startsWith('BRF:')); - - console.log('Found LF lines:', lfLines.length); - console.log('Found FNF lines:', fnfLines.length); - console.log('Found BRF lines:', brfLines.length); - - // Get the last occurrence of each (should be the totals) - if (lfLines.length > 0) { - const lastLF = lfLines[lfLines.length - 1]; - const match = lastLF.match(/LF:\s*(\d+)\s*LH:\s*(\d+)/); - if (match) { - totalLines = parseInt(match[1]); - coveredLines = parseInt(match[2]); - console.log('Total lines:', totalLines, 'Covered lines:', coveredLines); + // Sum up all individual file records + lines.forEach(line => { + if (line.startsWith('LF:')) { + const match = line.match(/LF:\s*(\d+)\s*LH:\s*(\d+)/); + if (match) { + totalLines += parseInt(match[1]); + coveredLines += parseInt(match[2]); + } } - } - - if (fnfLines.length > 0) { - const lastFNF = fnfLines[fnfLines.length - 1]; - const match = lastFNF.match(/FNF:\s*(\d+)\s*FNH:\s*(\d+)/); - if (match) { - totalFunctions = parseInt(match[1]); - coveredFunctions = parseInt(match[2]); - console.log('Total functions:', totalFunctions, 'Covered functions:', coveredFunctions); + if (line.startsWith('FNF:')) { + const match = line.match(/FNF:\s*(\d+)\s*FNH:\s*(\d+)/); + if (match) { + totalFunctions += parseInt(match[1]); + coveredFunctions += parseInt(match[2]); + } } - } - - if (brfLines.length > 0) { - const lastBRF = brfLines[brfLines.length - 1]; - const match = lastBRF.match(/BRF:\s*(\d+)\s*BRH:\s*(\d+)/); - if (match) { - totalBranches = parseInt(match[1]); - coveredBranches = parseInt(match[2]); - console.log('Total branches:', totalBranches, 'Covered branches:', coveredBranches); + if (line.startsWith('BRF:')) { + const match = line.match(/BRF:\s*(\d+)\s*BRH:\s*(\d+)/); + if (match) { + totalBranches += parseInt(match[1]); + coveredBranches += parseInt(match[2]); + } } - } + }); + + console.log('Summed totals - Lines:', totalLines, 'Covered:', coveredLines); + console.log('Summed totals - Functions:', totalFunctions, 'Covered:', coveredFunctions); + console.log('Summed totals - Branches:', totalBranches, 'Covered:', coveredBranches); const lineCoverage = totalLines > 0 ? Math.round((coveredLines / totalLines) * 100) : 0; const functionCoverage = totalFunctions > 0 ? Math.round((coveredFunctions / totalFunctions) * 100) : 0; From 7e6749ccbc74a3ca0ed77f3ec62400c8b241233f Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 15:36:01 -0400 Subject: [PATCH 14/20] fix again --- .github/workflows/coverage.yml | 41 +++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f0102519..4288826c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -60,30 +60,35 @@ jobs: let totalBranches = 0; let coveredBranches = 0; - // Sum up all individual file records - lines.forEach(line => { + // LF:, LH:, FNF:, FNH:, BRF:, BRH: are on separate lines + // We need to track them separately and match them up + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // Lines Found and Lines Hit if (line.startsWith('LF:')) { - const match = line.match(/LF:\s*(\d+)\s*LH:\s*(\d+)/); - if (match) { - totalLines += parseInt(match[1]); - coveredLines += parseInt(match[2]); - } + totalLines += parseInt(line.substring(3)) || 0; + } + if (line.startsWith('LH:')) { + coveredLines += parseInt(line.substring(3)) || 0; } + + // Functions Found and Functions Hit if (line.startsWith('FNF:')) { - const match = line.match(/FNF:\s*(\d+)\s*FNH:\s*(\d+)/); - if (match) { - totalFunctions += parseInt(match[1]); - coveredFunctions += parseInt(match[2]); - } + totalFunctions += parseInt(line.substring(4)) || 0; + } + if (line.startsWith('FNH:')) { + coveredFunctions += parseInt(line.substring(4)) || 0; } + + // Branches Found and Branches Hit if (line.startsWith('BRF:')) { - const match = line.match(/BRF:\s*(\d+)\s*BRH:\s*(\d+)/); - if (match) { - totalBranches += parseInt(match[1]); - coveredBranches += parseInt(match[2]); - } + totalBranches += parseInt(line.substring(4)) || 0; } - }); + if (line.startsWith('BRH:')) { + coveredBranches += parseInt(line.substring(4)) || 0; + } + } console.log('Summed totals - Lines:', totalLines, 'Covered:', coveredLines); console.log('Summed totals - Functions:', totalFunctions, 'Covered:', coveredFunctions); From 34935e5893c2144ae08ffb82a270a529e822cb95 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 15:38:34 -0400 Subject: [PATCH 15/20] update test to ignore .md files --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4481ec6a..89dc6424 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,8 @@ name: CI on: push: pull_request: + paths-ignore: + - '**.md' workflow_dispatch: env: From c0cf42fccbc143792c5cb4d3e351dbe96e9edc17 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 15:40:06 -0400 Subject: [PATCH 16/20] update test --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 89dc6424..84ad3b4f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,7 @@ name: CI on: push: + branches: [main] pull_request: paths-ignore: - '**.md' From e6120ba986fd584d57cb2f8c3510faab234c3ff2 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 15:44:00 -0400 Subject: [PATCH 17/20] extract coverage report scripts of action --- .github/scripts/coverage-comment.js | 142 ++++++++++++++++++++++++++++ .github/workflows/coverage.yml | 93 +----------------- .github/workflows/slither.yml | 4 + .github/workflows/test.yml | 2 +- 4 files changed, 149 insertions(+), 92 deletions(-) create mode 100644 .github/scripts/coverage-comment.js diff --git a/.github/scripts/coverage-comment.js b/.github/scripts/coverage-comment.js new file mode 100644 index 00000000..d500c245 --- /dev/null +++ b/.github/scripts/coverage-comment.js @@ -0,0 +1,142 @@ +const fs = require('fs'); + +// Configuration thresholds +const THRESHOLDS = { + good: 80, + needsImprovement: 60, + poor: 40 +}; + +/** + * Parse lcov.info file and extract coverage metrics + * @param {string} content - The lcov.info file content + * @returns {object} Coverage metrics + */ +function parseLcovContent(content) { + const lines = content.split('\n'); + let totalLines = 0; + let coveredLines = 0; + let totalFunctions = 0; + let coveredFunctions = 0; + let totalBranches = 0; + let coveredBranches = 0; + + // LF:, LH:, FNF:, FNH:, BRF:, BRH: are on separate lines + // We need to track them separately and sum them up + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // Lines Found and Lines Hit + if (line.startsWith('LF:')) { + totalLines += parseInt(line.substring(3)) || 0; + } + if (line.startsWith('LH:')) { + coveredLines += parseInt(line.substring(3)) || 0; + } + + // Functions Found and Functions Hit + if (line.startsWith('FNF:')) { + totalFunctions += parseInt(line.substring(4)) || 0; + } + if (line.startsWith('FNH:')) { + coveredFunctions += parseInt(line.substring(4)) || 0; + } + + // Branches Found and Branches Hit + if (line.startsWith('BRF:')) { + totalBranches += parseInt(line.substring(4)) || 0; + } + if (line.startsWith('BRH:')) { + coveredBranches += parseInt(line.substring(4)) || 0; + } + } + + return { + totalLines, + coveredLines, + totalFunctions, + coveredFunctions, + totalBranches, + coveredBranches + }; +} + +/** + * Calculate coverage percentage + * @param {number} covered - Number of covered items + * @param {number} total - Total number of items + * @returns {number} Coverage percentage + */ +function calculateCoverage(covered, total) { + return total > 0 ? Math.round((covered / total) * 100) : 0; +} + +/** + * Get badge color based on coverage percentage + * @param {number} coverage - Coverage percentage + * @returns {string} Badge color + */ +function getBadgeColor(coverage) { + if (coverage >= THRESHOLDS.good) return 'brightgreen'; + if (coverage >= THRESHOLDS.needsImprovement) return 'yellow'; + if (coverage >= THRESHOLDS.poor) return 'orange'; + return 'red'; +} + +/** + * Generate coverage report comment body + * @param {object} metrics - Coverage metrics + * @returns {string} Markdown formatted comment body + */ +function generateCoverageReport(metrics) { + const lineCoverage = calculateCoverage(metrics.coveredLines, metrics.totalLines); + const functionCoverage = calculateCoverage(metrics.coveredFunctions, metrics.totalFunctions); + const branchCoverage = calculateCoverage(metrics.coveredBranches, metrics.totalBranches); + + const badgeColor = getBadgeColor(lineCoverage); + const badge = `![Coverage](https://img.shields.io/badge/coverage-${lineCoverage}%25-${badgeColor})`; + + return `## Coverage Report\n` + + `${badge}\n\n` + + `| Metric | Coverage | Details |\n` + + `|--------|----------|----------|\n` + + `| **Lines** | ${lineCoverage}% | ${metrics.coveredLines}/${metrics.totalLines} lines |\n` + + `| **Functions** | ${functionCoverage}% | ${metrics.coveredFunctions}/${metrics.totalFunctions} functions |\n` + + `| **Branches** | ${branchCoverage}% | ${metrics.coveredBranches}/${metrics.totalBranches} branches |\n\n`; +} + +/** + * Main function to post coverage comment + * @param {object} github - GitHub API object + * @param {object} context - GitHub Actions context + */ +async function postCoverageComment(github, context) { + const file = 'lcov.info'; + + if (!fs.existsSync(file)) { + console.log('Coverage file not found.'); + return; + } + + const content = fs.readFileSync(file, 'utf8'); + const metrics = parseLcovContent(content); + + console.log('Coverage Metrics:'); + console.log('- Lines:', metrics.coveredLines, '/', metrics.totalLines); + console.log('- Functions:', metrics.coveredFunctions, '/', metrics.totalFunctions); + console.log('- Branches:', metrics.coveredBranches, '/', metrics.totalBranches); + + const body = generateCoverageReport(metrics); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + + console.log('Coverage comment posted successfully!'); +} + +module.exports = { postCoverageComment }; + diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 4288826c..a4a7251f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -34,94 +34,5 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const fs = require('fs'); - const file = 'lcov.info'; - let pct = '-'; - if (fs.existsSync(file)) { - const content = fs.readFileSync(file, 'utf8'); - const match = content.match(/LF:\s*(\d+)\s*LH:\s*(\d+)/); - if (match) { - const total = Number(match[1]); - const covered = Number(match[2]); - pct = Math.round((covered / total)*100) + '%'; - } - - // THIS IS THE THRESHOLDS - const goodThreshold = 80; - const needsImprovementThreshold = 60; - const poorThreshold = 40; - - // Parse more detailed coverage info - sum all individual file records - const lines = content.split('\n'); - let totalLines = 0; - let coveredLines = 0; - let totalFunctions = 0; - let coveredFunctions = 0; - let totalBranches = 0; - let coveredBranches = 0; - - // LF:, LH:, FNF:, FNH:, BRF:, BRH: are on separate lines - // We need to track them separately and match them up - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - - // Lines Found and Lines Hit - if (line.startsWith('LF:')) { - totalLines += parseInt(line.substring(3)) || 0; - } - if (line.startsWith('LH:')) { - coveredLines += parseInt(line.substring(3)) || 0; - } - - // Functions Found and Functions Hit - if (line.startsWith('FNF:')) { - totalFunctions += parseInt(line.substring(4)) || 0; - } - if (line.startsWith('FNH:')) { - coveredFunctions += parseInt(line.substring(4)) || 0; - } - - // Branches Found and Branches Hit - if (line.startsWith('BRF:')) { - totalBranches += parseInt(line.substring(4)) || 0; - } - if (line.startsWith('BRH:')) { - coveredBranches += parseInt(line.substring(4)) || 0; - } - } - - console.log('Summed totals - Lines:', totalLines, 'Covered:', coveredLines); - console.log('Summed totals - Functions:', totalFunctions, 'Covered:', coveredFunctions); - console.log('Summed totals - Branches:', totalBranches, 'Covered:', coveredBranches); - - const lineCoverage = totalLines > 0 ? Math.round((coveredLines / totalLines) * 100) : 0; - const functionCoverage = totalFunctions > 0 ? Math.round((coveredFunctions / totalFunctions) * 100) : 0; - const branchCoverage = totalBranches > 0 ? Math.round((coveredBranches / totalBranches) * 100) : 0; - - // Create coverage badge - const getBadgeColor = (coverage) => { - if (coverage >= goodThreshold) return 'brightgreen'; - if (coverage >= needsImprovementThreshold) return 'yellow'; - if (coverage >= poorThreshold) return 'orange'; - return 'red'; - }; - - const badgeColor = getBadgeColor(lineCoverage); - const badge = `![Coverage](https://img.shields.io/badge/coverage-${lineCoverage}%25-${badgeColor})`; - - const body = `## Coverage Report\n` + - `${badge}\n\n` + - `| Metric | Coverage | Details |\n` + - `|--------|----------|----------|\n` + - `| **Lines** | ${lineCoverage}% | ${coveredLines}/${totalLines} lines |\n` + - `| **Functions** | ${functionCoverage}% | ${coveredFunctions}/${totalFunctions} functions |\n` + - `| **Branches** | ${branchCoverage}% | ${coveredBranches}/${totalBranches} branches |\n\n`; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body - }); - } else { - console.log('Coverage file not found.'); - } + const { postCoverageComment } = require('./.github/scripts/coverage-comment.js'); + await postCoverageComment(github, context); diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml index f2e32020..af6aedfd 100644 --- a/.github/workflows/slither.yml +++ b/.github/workflows/slither.yml @@ -1,8 +1,12 @@ name: CI + on: push: + branches: [main] pull_request: + paths-ignore: + - '**.md' workflow_dispatch: env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84ad3b4f..9dce09e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ env: jobs: check: - name: Foundry project + name: Foundry Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From c36966059d92b67eab2cbb460278272506f0455f Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 16:25:22 -0400 Subject: [PATCH 18/20] separate the coverage and the comment posting for security access --- .github/scripts/coverage-comment.js | 31 ++++++++++- .github/workflows/coverage-comment.yml | 73 ++++++++++++++++++++++++++ .github/workflows/coverage.yml | 19 ++++--- 3 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/coverage-comment.yml diff --git a/.github/scripts/coverage-comment.js b/.github/scripts/coverage-comment.js index d500c245..c21ae7ff 100644 --- a/.github/scripts/coverage-comment.js +++ b/.github/scripts/coverage-comment.js @@ -138,5 +138,34 @@ async function postCoverageComment(github, context) { console.log('Coverage comment posted successfully!'); } -module.exports = { postCoverageComment }; +/** + * Generate coverage report and save to file (for workflow artifacts) + */ +function generateCoverageFile() { + const file = 'lcov.info'; + + if (!fs.existsSync(file)) { + console.log('Coverage file not found.'); + return; + } + + const content = fs.readFileSync(file, 'utf8'); + const metrics = parseLcovContent(content); + + console.log('Coverage Metrics:'); + console.log('- Lines:', metrics.coveredLines, '/', metrics.totalLines); + console.log('- Functions:', metrics.coveredFunctions, '/', metrics.totalFunctions); + console.log('- Branches:', metrics.coveredBranches, '/', metrics.totalBranches); + + const body = generateCoverageReport(metrics); + fs.writeFileSync('coverage-report.md', body); + console.log('Coverage report saved to coverage-report.md'); +} + +// If run directly (not as module), generate the file +if (require.main === module) { + generateCoverageFile(); +} + +module.exports = { postCoverageComment, generateCoverageFile }; diff --git a/.github/workflows/coverage-comment.yml b/.github/workflows/coverage-comment.yml new file mode 100644 index 00000000..42325624 --- /dev/null +++ b/.github/workflows/coverage-comment.yml @@ -0,0 +1,73 @@ +name: Post Coverage Comment + +on: + workflow_run: + workflows: ["CI"] + types: + - completed + +permissions: + pull-requests: write + issues: write + +jobs: + comment: + name: Post Coverage Comment + runs-on: ubuntu-latest + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + steps: + - name: Download coverage data + uses: actions/download-artifact@v4 + with: + name: coverage-data + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Extract PR number + id: pr + run: | + PR_NUMBER=$(cat coverage-data.txt | grep PR_NUMBER | cut -d'=' -f2) + echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT + + - name: Post coverage comment + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const prNumber = ${{ steps.pr.outputs.number }}; + const body = fs.readFileSync('coverage-report.md', 'utf8'); + + // Check if a coverage comment already exists + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + const botComment = comments.data.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('## Coverage Report') + ); + + if (botComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + console.log('Coverage comment updated successfully!'); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: body + }); + console.log('Coverage comment posted successfully!'); + } diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a4a7251f..85a0ac6b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -28,11 +28,18 @@ jobs: forge coverage --report summary --report lcov ls -la lcov.info || echo "lcov.info not found" - - name: Comment on PR + - name: Generate coverage report if: github.event_name == 'pull_request' - uses: actions/github-script@v7 + run: | + node .github/scripts/coverage-comment.js + echo "PR_NUMBER=${{ github.event.pull_request.number }}" >> coverage-data.txt + + - name: Upload coverage data + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { postCoverageComment } = require('./.github/scripts/coverage-comment.js'); - await postCoverageComment(github, context); + name: coverage-data + path: | + coverage-report.md + coverage-data.txt + retention-days: 1 From 1cb8be3d8f429640fbdee4c238661dee04b5c069 Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 16:31:58 -0400 Subject: [PATCH 19/20] fix comment action --- .github/workflows/coverage-comment.yml | 76 +++++++++++++++++++------- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/.github/workflows/coverage-comment.yml b/.github/workflows/coverage-comment.yml index 42325624..7c99e610 100644 --- a/.github/workflows/coverage-comment.yml +++ b/.github/workflows/coverage-comment.yml @@ -18,40 +18,78 @@ jobs: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - - name: Download coverage data - uses: actions/download-artifact@v4 - with: - name: coverage-data - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - - name: Extract PR number - id: pr - run: | - PR_NUMBER=$(cat coverage-data.txt | grep PR_NUMBER | cut -d'=' -f2) - echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT - - - name: Post coverage comment + - name: Download and post coverage comment uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); - const prNumber = ${{ steps.pr.outputs.number }}; - const body = fs.readFileSync('coverage-report.md', 'utf8'); - + const path = require('path'); + const { execSync } = require('child_process'); + + // Download artifact + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + + const coverageArtifact = artifacts.data.artifacts.find( + artifact => artifact.name === 'coverage-data' + ); + + if (!coverageArtifact) { + console.log('No coverage artifact found'); + return; + } + + console.log('Found coverage artifact, downloading...'); + + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: coverageArtifact.id, + archive_format: 'zip', + }); + + // Save and extract the artifact using execSync + const artifactPath = path.join(process.env.GITHUB_WORKSPACE, 'coverage-data.zip'); + fs.writeFileSync(artifactPath, Buffer.from(download.data)); + + console.log('Artifact downloaded, extracting...'); + + // Unzip the artifact + execSync(`unzip -o ${artifactPath} -d ${process.env.GITHUB_WORKSPACE}`); + + // Extract PR number + const prDataPath = path.join(process.env.GITHUB_WORKSPACE, 'coverage-data.txt'); + const prData = fs.readFileSync(prDataPath, 'utf8'); + const prMatch = prData.match(/PR_NUMBER=(\d+)/); + + if (!prMatch) { + console.log('Could not find PR number in coverage-data.txt'); + return; + } + + const prNumber = parseInt(prMatch[1]); + console.log(`Processing coverage for PR #${prNumber}`); + + // Read coverage report + const reportPath = path.join(process.env.GITHUB_WORKSPACE, 'coverage-report.md'); + const body = fs.readFileSync(reportPath, 'utf8'); + // Check if a coverage comment already exists const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); - + const botComment = comments.data.find(comment => comment.user.type === 'Bot' && comment.body.includes('## Coverage Report') ); - + if (botComment) { // Update existing comment await github.rest.issues.updateComment({ From 730b9967b8217ac512ef7a57e7c90f4c783e4cdd Mon Sep 17 00:00:00 2001 From: Maxime Normandin Date: Fri, 24 Oct 2025 16:39:25 -0400 Subject: [PATCH 20/20] update coverage ci --- .github/workflows/coverage-comment.yml | 17 +++++++++++++---- .github/workflows/coverage.yml | 1 + 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coverage-comment.yml b/.github/workflows/coverage-comment.yml index 7c99e610..fdadc92f 100644 --- a/.github/workflows/coverage-comment.yml +++ b/.github/workflows/coverage-comment.yml @@ -7,18 +7,27 @@ on: - completed permissions: + actions: read pull-requests: write issues: write + contents: read jobs: comment: name: Post Coverage Comment runs-on: ubuntu-latest - if: > - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.conclusion == 'success' + # Only run if the workflow run was for a pull request + if: github.event.workflow_run.event == 'pull_request' steps: + - name: Check workflow run status + run: | + echo "Workflow run conclusion: ${{ github.event.workflow_run.conclusion }}" + echo "Workflow run event: ${{ github.event.workflow_run.event }}" + echo "Workflow run ID: ${{ github.event.workflow_run.id }}" + echo "Head branch: ${{ github.event.workflow_run.head_branch }}" + - name: Download and post coverage comment + if: github.event.workflow_run.conclusion == 'success' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -108,4 +117,4 @@ jobs: body: body }); console.log('Coverage comment posted successfully!'); - } + } \ No newline at end of file diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 85a0ac6b..99b3aa63 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,6 +33,7 @@ jobs: run: | node .github/scripts/coverage-comment.js echo "PR_NUMBER=${{ github.event.pull_request.number }}" >> coverage-data.txt + echo "Generated coverage report for PR #${{ github.event.pull_request.number }}" - name: Upload coverage data if: github.event_name == 'pull_request'