Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions .github/scripts/coverage-comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
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!');
}

/**
* 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 };

120 changes: 120 additions & 0 deletions .github/workflows/coverage-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: Post Coverage Comment

on:
workflow_run:
workflows: ["CI"]
types:
- completed

permissions:
actions: read
pull-requests: write
issues: write
contents: read

jobs:
comment:
name: Post Coverage Comment
runs-on: ubuntu-latest
# 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 }}
script: |
const fs = require('fs');
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({
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!');
}
46 changes: 46 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: CI

on:
push:
branches: [main]
pull_request:
paths-ignore:
- '**.md'
workflow_dispatch:

env:
FOUNDRY_PROFILE: ci

jobs:
check:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Run coverage
run: |
forge coverage --report summary --report lcov
ls -la lcov.info || echo "lcov.info not found"

- name: Generate coverage report
if: github.event_name == 'pull_request'
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'
uses: actions/upload-artifact@v4
with:
name: coverage-data
path: |
coverage-report.md
coverage-data.txt
retention-days: 1
36 changes: 36 additions & 0 deletions .github/workflows/slither.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI


on:
push:
branches: [main]
pull_request:
paths-ignore:
- '**.md'
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
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ name: CI

on:
push:
branches: [main]
pull_request:
paths-ignore:
- '**.md'
workflow_dispatch:

env:
FOUNDRY_PROFILE: ci

jobs:
check:
name: Foundry project
name: Foundry Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
Loading
Loading