Skip to content

Commit 27993fe

Browse files
committed
Merge branch 'main' into feat/erc20-test-suite
2 parents 781b760 + edd2d31 commit 27993fe

27 files changed

+739
-32
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
const fs = require('fs');
2+
3+
// Configuration thresholds
4+
const THRESHOLDS = {
5+
good: 80,
6+
needsImprovement: 60,
7+
poor: 40
8+
};
9+
10+
/**
11+
* Parse lcov.info file and extract coverage metrics
12+
* @param {string} content - The lcov.info file content
13+
* @returns {object} Coverage metrics
14+
*/
15+
function parseLcovContent(content) {
16+
const lines = content.split('\n');
17+
let totalLines = 0;
18+
let coveredLines = 0;
19+
let totalFunctions = 0;
20+
let coveredFunctions = 0;
21+
let totalBranches = 0;
22+
let coveredBranches = 0;
23+
24+
// LF:, LH:, FNF:, FNH:, BRF:, BRH: are on separate lines
25+
// We need to track them separately and sum them up
26+
for (let i = 0; i < lines.length; i++) {
27+
const line = lines[i].trim();
28+
29+
// Lines Found and Lines Hit
30+
if (line.startsWith('LF:')) {
31+
totalLines += parseInt(line.substring(3)) || 0;
32+
}
33+
if (line.startsWith('LH:')) {
34+
coveredLines += parseInt(line.substring(3)) || 0;
35+
}
36+
37+
// Functions Found and Functions Hit
38+
if (line.startsWith('FNF:')) {
39+
totalFunctions += parseInt(line.substring(4)) || 0;
40+
}
41+
if (line.startsWith('FNH:')) {
42+
coveredFunctions += parseInt(line.substring(4)) || 0;
43+
}
44+
45+
// Branches Found and Branches Hit
46+
if (line.startsWith('BRF:')) {
47+
totalBranches += parseInt(line.substring(4)) || 0;
48+
}
49+
if (line.startsWith('BRH:')) {
50+
coveredBranches += parseInt(line.substring(4)) || 0;
51+
}
52+
}
53+
54+
return {
55+
totalLines,
56+
coveredLines,
57+
totalFunctions,
58+
coveredFunctions,
59+
totalBranches,
60+
coveredBranches
61+
};
62+
}
63+
64+
/**
65+
* Calculate coverage percentage
66+
* @param {number} covered - Number of covered items
67+
* @param {number} total - Total number of items
68+
* @returns {number} Coverage percentage
69+
*/
70+
function calculateCoverage(covered, total) {
71+
return total > 0 ? Math.round((covered / total) * 100) : 0;
72+
}
73+
74+
/**
75+
* Get badge color based on coverage percentage
76+
* @param {number} coverage - Coverage percentage
77+
* @returns {string} Badge color
78+
*/
79+
function getBadgeColor(coverage) {
80+
if (coverage >= THRESHOLDS.good) return 'brightgreen';
81+
if (coverage >= THRESHOLDS.needsImprovement) return 'yellow';
82+
if (coverage >= THRESHOLDS.poor) return 'orange';
83+
return 'red';
84+
}
85+
86+
/**
87+
* Generate coverage report comment body
88+
* @param {object} metrics - Coverage metrics
89+
* @param {object} commitInfo - Optional commit information
90+
* @returns {string} Markdown formatted comment body
91+
*/
92+
function generateCoverageReport(metrics, commitInfo = {}) {
93+
const lineCoverage = calculateCoverage(metrics.coveredLines, metrics.totalLines);
94+
const functionCoverage = calculateCoverage(metrics.coveredFunctions, metrics.totalFunctions);
95+
const branchCoverage = calculateCoverage(metrics.coveredBranches, metrics.totalBranches);
96+
97+
const badgeColor = getBadgeColor(lineCoverage);
98+
const badge = `![Coverage](https://img.shields.io/badge/coverage-${lineCoverage}%25-${badgeColor})`;
99+
100+
// Generate timestamp
101+
const timestamp = new Date().toUTCString();
102+
103+
// Build commit link if info is available
104+
let commitLink = '';
105+
if (commitInfo.sha && commitInfo.owner && commitInfo.repo) {
106+
const shortSha = commitInfo.sha.substring(0, 7);
107+
commitLink = ` for commit [\`${shortSha}\`](https://github.com/${commitInfo.owner}/${commitInfo.repo}/commit/${commitInfo.sha})`;
108+
}
109+
110+
return `## Coverage Report\n` +
111+
`${badge}\n\n` +
112+
`| Metric | Coverage | Details |\n` +
113+
`|--------|----------|----------|\n` +
114+
`| **Lines** | ${lineCoverage}% | ${metrics.coveredLines}/${metrics.totalLines} lines |\n` +
115+
`| **Functions** | ${functionCoverage}% | ${metrics.coveredFunctions}/${metrics.totalFunctions} functions |\n` +
116+
`| **Branches** | ${branchCoverage}% | ${metrics.coveredBranches}/${metrics.totalBranches} branches |\n\n` +
117+
`*Last updated: ${timestamp}*${commitLink}\n`;
118+
}
119+
120+
/**
121+
* Main function to post coverage comment
122+
* @param {object} github - GitHub API object
123+
* @param {object} context - GitHub Actions context
124+
*/
125+
async function postCoverageComment(github, context) {
126+
const file = 'lcov.info';
127+
128+
if (!fs.existsSync(file)) {
129+
console.log('Coverage file not found.');
130+
return;
131+
}
132+
133+
const content = fs.readFileSync(file, 'utf8');
134+
const metrics = parseLcovContent(content);
135+
136+
console.log('Coverage Metrics:');
137+
console.log('- Lines:', metrics.coveredLines, '/', metrics.totalLines);
138+
console.log('- Functions:', metrics.coveredFunctions, '/', metrics.totalFunctions);
139+
console.log('- Branches:', metrics.coveredBranches, '/', metrics.totalBranches);
140+
141+
const body = generateCoverageReport(metrics);
142+
143+
await github.rest.issues.createComment({
144+
owner: context.repo.owner,
145+
repo: context.repo.repo,
146+
issue_number: context.issue.number,
147+
body: body
148+
});
149+
150+
console.log('Coverage comment posted successfully!');
151+
}
152+
153+
/**
154+
* Generate coverage report and save to file (for workflow artifacts)
155+
*/
156+
function generateCoverageFile() {
157+
const file = 'lcov.info';
158+
159+
if (!fs.existsSync(file)) {
160+
console.log('Coverage file not found.');
161+
return;
162+
}
163+
164+
const content = fs.readFileSync(file, 'utf8');
165+
const metrics = parseLcovContent(content);
166+
167+
console.log('Coverage Metrics:');
168+
console.log('- Lines:', metrics.coveredLines, '/', metrics.totalLines);
169+
console.log('- Functions:', metrics.coveredFunctions, '/', metrics.totalFunctions);
170+
console.log('- Branches:', metrics.coveredBranches, '/', metrics.totalBranches);
171+
172+
// Get commit info from environment variables
173+
const commitInfo = {
174+
sha: process.env.COMMIT_SHA,
175+
owner: process.env.REPO_OWNER,
176+
repo: process.env.REPO_NAME
177+
};
178+
179+
const body = generateCoverageReport(metrics, commitInfo);
180+
fs.writeFileSync('coverage-report.md', body);
181+
console.log('Coverage report saved to coverage-report.md');
182+
}
183+
184+
// If run directly (not as module), generate the file
185+
if (require.main === module) {
186+
generateCoverageFile();
187+
}
188+
189+
module.exports = { postCoverageComment, generateCoverageFile };
190+
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Post coverage comment workflow script
3+
* Downloads coverage artifact from a workflow run and posts/updates PR comment
4+
*
5+
* This script is designed to run in a workflow_run triggered workflow
6+
* with proper permissions to comment on PRs from forks.
7+
*/
8+
9+
module.exports = async ({ github, context }) => {
10+
const fs = require('fs');
11+
const path = require('path');
12+
const { execSync } = require('child_process');
13+
14+
console.log('Starting coverage comment posting process...');
15+
16+
// Download artifact
17+
console.log('Fetching artifacts from workflow run...');
18+
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
19+
owner: context.repo.owner,
20+
repo: context.repo.repo,
21+
run_id: context.payload.workflow_run.id,
22+
});
23+
24+
const coverageArtifact = artifacts.data.artifacts.find(
25+
artifact => artifact.name === 'coverage-data'
26+
);
27+
28+
if (!coverageArtifact) {
29+
console.log('No coverage artifact found');
30+
console.log('Available artifacts:', artifacts.data.artifacts.map(a => a.name).join(', '));
31+
return;
32+
}
33+
34+
console.log('Found coverage artifact, downloading...');
35+
36+
const download = await github.rest.actions.downloadArtifact({
37+
owner: context.repo.owner,
38+
repo: context.repo.repo,
39+
artifact_id: coverageArtifact.id,
40+
archive_format: 'zip',
41+
});
42+
43+
// Save and extract the artifact using execSync
44+
const artifactPath = path.join(process.env.GITHUB_WORKSPACE, 'coverage-data.zip');
45+
fs.writeFileSync(artifactPath, Buffer.from(download.data));
46+
47+
console.log('Artifact downloaded, extracting...');
48+
49+
// Unzip the artifact
50+
execSync(`unzip -o ${artifactPath} -d ${process.env.GITHUB_WORKSPACE}`);
51+
52+
// Extract PR number
53+
const prDataPath = path.join(process.env.GITHUB_WORKSPACE, 'coverage-data.txt');
54+
55+
if (!fs.existsSync(prDataPath)) {
56+
console.log('coverage-data.txt not found in artifact');
57+
console.log('Extracted files:', execSync(`ls -la ${process.env.GITHUB_WORKSPACE}`).toString());
58+
return;
59+
}
60+
61+
const prData = fs.readFileSync(prDataPath, 'utf8');
62+
const prMatch = prData.match(/PR_NUMBER=(\d+)/);
63+
64+
if (!prMatch) {
65+
console.log('Could not find PR number in coverage-data.txt');
66+
console.log('File contents:', prData);
67+
return;
68+
}
69+
70+
const prNumber = parseInt(prMatch[1]);
71+
console.log(`Processing coverage for PR #${prNumber}`);
72+
73+
// Read coverage report
74+
const reportPath = path.join(process.env.GITHUB_WORKSPACE, 'coverage-report.md');
75+
76+
if (!fs.existsSync(reportPath)) {
77+
console.log("coverage-report.md not found in artifact");
78+
return;
79+
}
80+
81+
const body = fs.readFileSync(reportPath, 'utf8');
82+
console.log('✓ Coverage report loaded');
83+
84+
// Check if a coverage comment already exists
85+
console.log('Checking for existing coverage comments...');
86+
const comments = await github.rest.issues.listComments({
87+
owner: context.repo.owner,
88+
repo: context.repo.repo,
89+
issue_number: prNumber
90+
});
91+
92+
const botComment = comments.data.find(comment =>
93+
comment.user.type === 'Bot' &&
94+
comment.body.includes('## Coverage Report')
95+
);
96+
97+
if (botComment) {
98+
// Update existing comment
99+
console.log(`Updating existing comment (ID: ${botComment.id})...`);
100+
await github.rest.issues.updateComment({
101+
owner: context.repo.owner,
102+
repo: context.repo.repo,
103+
comment_id: botComment.id,
104+
body: body
105+
});
106+
console.log("Coverage comment updated successfully!");
107+
} else {
108+
// Create new comment
109+
console.log('Creating new coverage comment...');
110+
await github.rest.issues.createComment({
111+
owner: context.repo.owner,
112+
repo: context.repo.repo,
113+
issue_number: prNumber,
114+
body: body
115+
});
116+
console.log("Coverage comment posted successfully!");
117+
}
118+
};
119+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Comment Report on Coverage
2+
3+
on:
4+
workflow_run:
5+
workflows: ["Coverage"]
6+
types:
7+
- completed
8+
9+
permissions:
10+
actions: read
11+
pull-requests: write
12+
issues: write
13+
contents: read
14+
15+
jobs:
16+
comment:
17+
name: Post Coverage Comment
18+
runs-on: ubuntu-latest
19+
# Only run if the workflow run was for a pull request
20+
if: github.event.workflow_run.event == 'pull_request'
21+
steps:
22+
- name: Check workflow run status
23+
run: |
24+
echo "Workflow run conclusion: ${{ github.event.workflow_run.conclusion }}"
25+
echo "Workflow run event: ${{ github.event.workflow_run.event }}"
26+
echo "Workflow run ID: ${{ github.event.workflow_run.id }}"
27+
echo "Head branch: ${{ github.event.workflow_run.head_branch }}"
28+
29+
- name: Checkout repository
30+
if: github.event.workflow_run.conclusion == 'success'
31+
uses: actions/checkout@v4
32+
33+
- name: Download and post coverage comment
34+
if: github.event.workflow_run.conclusion == 'success'
35+
uses: actions/github-script@v7
36+
with:
37+
github-token: ${{ secrets.GITHUB_TOKEN }}
38+
script: |
39+
const script = require('./.github/scripts/post-coverage-comment.js');
40+
await script({ github, context });

0 commit comments

Comments
 (0)