Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bcb6941
add coverage ci
maxnorm Oct 24, 2025
13fc65b
add slither ci
maxnorm Oct 24, 2025
a786cb7
fix coverage
maxnorm Oct 24, 2025
6ce60cf
fix coverage ci again
maxnorm Oct 24, 2025
a7e3cab
rework on coverage
maxnorm Oct 24, 2025
44146b3
adjust coverage actions to comment only on PR
maxnorm Oct 24, 2025
06660bc
update coverage report
maxnorm Oct 24, 2025
4f02cce
update again
maxnorm Oct 24, 2025
bf628c3
update coverage action for better output
maxnorm Oct 24, 2025
8b42eeb
get totals
maxnorm Oct 24, 2025
51c3d03
fix syntax
maxnorm Oct 24, 2025
d31f8cc
test
maxnorm Oct 24, 2025
9eb749a
test 2
maxnorm Oct 24, 2025
7e6749c
fix again
maxnorm Oct 24, 2025
34935e5
update test to ignore .md files
maxnorm Oct 24, 2025
c0cf42f
update test
maxnorm Oct 24, 2025
e6120ba
extract coverage report scripts of action
maxnorm Oct 24, 2025
c369660
separate the coverage and the comment posting for security access
maxnorm Oct 24, 2025
1cb8be3
fix comment action
maxnorm Oct 24, 2025
730b996
update coverage ci
maxnorm Oct 24, 2025
cf9cee0
Merge pull request #1 from maxnorm/feat/improve-ci
maxnorm Oct 24, 2025
9e73a68
extract script from actions
maxnorm Oct 24, 2025
ec6dc9e
update action name
maxnorm Oct 24, 2025
426b715
remove secon check name
maxnorm Oct 24, 2025
c593a52
update workflow_run name
maxnorm Oct 24, 2025
56d9d3a
Merge pull request #2 from maxnorm/feat/improve-ci
maxnorm Oct 24, 2025
12c5f5c
dummy change for test PR
maxnorm Oct 24, 2025
566a459
dummy 2
maxnorm Oct 24, 2025
0280318
add timestamp to comment since we are updating it
maxnorm Oct 24, 2025
4d6dae0
add last commit link to comment
maxnorm Oct 24, 2025
284566e
Merge pull request #3 from maxnorm/feat/improve-ci
maxnorm Oct 24, 2025
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
190 changes: 190 additions & 0 deletions .github/scripts/coverage-comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
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
* @param {object} commitInfo - Optional commit information
* @returns {string} Markdown formatted comment body
*/
function generateCoverageReport(metrics, commitInfo = {}) {
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})`;

// Generate timestamp
const timestamp = new Date().toUTCString();

// Build commit link if info is available
let commitLink = '';
if (commitInfo.sha && commitInfo.owner && commitInfo.repo) {
const shortSha = commitInfo.sha.substring(0, 7);
commitLink = ` for commit [\`${shortSha}\`](https://github.com/${commitInfo.owner}/${commitInfo.repo}/commit/${commitInfo.sha})`;
}

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` +
`*Last updated: ${timestamp}*${commitLink}\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);

// Get commit info from environment variables
const commitInfo = {
sha: process.env.COMMIT_SHA,
owner: process.env.REPO_OWNER,
repo: process.env.REPO_NAME
};

const body = generateCoverageReport(metrics, commitInfo);
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 };

119 changes: 119 additions & 0 deletions .github/scripts/post-coverage-comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* Post coverage comment workflow script
* Downloads coverage artifact from a workflow run and posts/updates PR comment
*
* This script is designed to run in a workflow_run triggered workflow
* with proper permissions to comment on PRs from forks.
*/

module.exports = async ({ github, context }) => {
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

console.log('Starting coverage comment posting process...');

// Download artifact
console.log('Fetching artifacts from workflow run...');
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');
console.log('Available artifacts:', artifacts.data.artifacts.map(a => a.name).join(', '));
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');

if (!fs.existsSync(prDataPath)) {
console.log('coverage-data.txt not found in artifact');
console.log('Extracted files:', execSync(`ls -la ${process.env.GITHUB_WORKSPACE}`).toString());
return;
}

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');
console.log('File contents:', prData);
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');

if (!fs.existsSync(reportPath)) {
console.log("coverage-report.md not found in artifact");
return;
}

const body = fs.readFileSync(reportPath, 'utf8');
console.log('✓ Coverage report loaded');

// Check if a coverage comment already exists
console.log('Checking for existing coverage comments...');
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
console.log(`Updating existing comment (ID: ${botComment.id})...`);
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
console.log('Creating new coverage 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!");
}
};

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

on:
workflow_run:
workflows: ["Coverage"]
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: Checkout repository
if: github.event.workflow_run.conclusion == 'success'
uses: actions/checkout@v4

- 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 script = require('./.github/scripts/post-coverage-comment.js');
await script({ github, context });
Loading
Loading