mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
CI sticky notes
This commit is contained in:
parent
c507b4b197
commit
eb74eb8590
4 changed files with 369 additions and 255 deletions
344
.github/workflows/ci-report.yml
vendored
Normal file
344
.github/workflows/ci-report.yml
vendored
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
name: CI Report
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['CI']
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
pr-report:
|
||||
name: PR Report
|
||||
if: >-
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion != 'cancelled'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Download PR metadata
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }},
|
||||
});
|
||||
|
||||
const meta = artifacts.data.artifacts.find(a => a.name === 'pr-meta');
|
||||
if (!meta) {
|
||||
core.setFailed('pr-meta artifact not found — skipping report');
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: meta.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
|
||||
const dest = path.join(process.env.RUNNER_TEMP, 'pr-meta');
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
fs.writeFileSync(path.join(dest, 'pr-meta.zip'), Buffer.from(zip.data));
|
||||
|
||||
- name: Extract PR metadata
|
||||
id: meta
|
||||
shell: bash
|
||||
run: |
|
||||
cd "$RUNNER_TEMP/pr-meta"
|
||||
unzip -o pr-meta.zip
|
||||
|
||||
PR_NUMBER=$(cat pr-number | tr -d '[:space:]')
|
||||
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
|
||||
echo "::error::Invalid PR number: '$PR_NUMBER'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "pr-number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
echo "quality=$(cat quality-result | tr -d '[:space:]')" >> "$GITHUB_OUTPUT"
|
||||
echo "tests=$(cat tests-result | tr -d '[:space:]')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download test reports
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }},
|
||||
});
|
||||
|
||||
const reports = artifacts.data.artifacts.find(a => a.name === 'test-reports');
|
||||
if (!reports) {
|
||||
core.warning('test-reports artifact not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: reports.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
|
||||
const dest = path.join(process.env.RUNNER_TEMP, 'test-reports');
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
fs.writeFileSync(path.join(dest, 'test-reports.zip'), Buffer.from(zip.data));
|
||||
|
||||
- name: Extract test reports
|
||||
shell: bash
|
||||
run: |
|
||||
cd "$RUNNER_TEMP/test-reports"
|
||||
unzip -o test-reports.zip || true
|
||||
|
||||
- name: Fetch cross-platform job results
|
||||
id: jobs
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const jobs = await github.rest.actions.listJobsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }},
|
||||
per_page: 50,
|
||||
});
|
||||
|
||||
const results = {};
|
||||
for (const job of jobs.data.jobs) {
|
||||
if (job.name.includes('ubuntu')) results.ubuntu = job.conclusion || 'pending';
|
||||
else if (job.name.includes('windows')) results.windows = job.conclusion || 'pending';
|
||||
else if (job.name.includes('macos')) results.macos = job.conclusion || 'pending';
|
||||
}
|
||||
core.setOutput('ubuntu', results.ubuntu || 'unknown');
|
||||
core.setOutput('windows', results.windows || 'unknown');
|
||||
core.setOutput('macos', results.macos || 'unknown');
|
||||
|
||||
- name: Fetch base branch coverage
|
||||
id: base-coverage
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'ci.yml',
|
||||
branch: 'main',
|
||||
status: 'success',
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
if (runs.data.workflow_runs.length === 0) {
|
||||
core.setOutput('found', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
const mainRunId = runs.data.workflow_runs[0].id;
|
||||
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: mainRunId,
|
||||
});
|
||||
|
||||
const testReports = artifacts.data.artifacts.find(a => a.name === 'test-reports');
|
||||
if (!testReports) {
|
||||
core.setOutput('found', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: testReports.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
|
||||
const dest = path.join(process.env.RUNNER_TEMP, 'base-coverage');
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
fs.writeFileSync(path.join(dest, 'base.zip'), Buffer.from(zip.data));
|
||||
core.setOutput('found', 'true');
|
||||
core.setOutput('dir', dest);
|
||||
|
||||
- name: Extract base coverage
|
||||
if: steps.base-coverage.outputs.found == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
cd "${{ steps.base-coverage.outputs.dir }}"
|
||||
unzip -o base.zip -d base
|
||||
|
||||
- name: Build and post report
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.meta.outputs.pr-number }}
|
||||
QUALITY: ${{ steps.meta.outputs.quality }}
|
||||
TESTS: ${{ steps.meta.outputs.tests }}
|
||||
UBUNTU: ${{ steps.jobs.outputs.ubuntu }}
|
||||
WINDOWS: ${{ steps.jobs.outputs.windows }}
|
||||
MACOS: ${{ steps.jobs.outputs.macos }}
|
||||
BASE_FOUND: ${{ steps.base-coverage.outputs.found }}
|
||||
BASE_DIR: ${{ steps.base-coverage.outputs.dir }}
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const icon = (s) => ({ success: '✅', failure: '❌', cancelled: '⏭️' }[s] || '❓');
|
||||
const temp = process.env.RUNNER_TEMP;
|
||||
|
||||
// ── Read coverage ──
|
||||
function readCov(dir) {
|
||||
const out = { stmts: 'N/A', branch: 'N/A', funcs: 'N/A', lines: 'N/A',
|
||||
stmtsCov: '', branchCov: '', funcsCov: '', linesCov: '' };
|
||||
try {
|
||||
const files = require('child_process')
|
||||
.execSync(`find "${dir}" -name coverage-summary.json -type f`, { encoding: 'utf8' })
|
||||
.trim().split('\n').filter(Boolean);
|
||||
if (!files.length) return out;
|
||||
const d = JSON.parse(fs.readFileSync(files[0], 'utf8')).total;
|
||||
out.stmts = d.statements.pct; out.branch = d.branches.pct;
|
||||
out.funcs = d.functions.pct; out.lines = d.lines.pct;
|
||||
out.stmtsCov = `${d.statements.covered}/${d.statements.total}`;
|
||||
out.branchCov = `${d.branches.covered}/${d.branches.total}`;
|
||||
out.funcsCov = `${d.functions.covered}/${d.functions.total}`;
|
||||
out.linesCov = `${d.lines.covered}/${d.lines.total}`;
|
||||
} catch {}
|
||||
return out;
|
||||
}
|
||||
|
||||
const cov = readCov(path.join(temp, 'test-reports'));
|
||||
const base = process.env.BASE_FOUND === 'true'
|
||||
? readCov(path.join(process.env.BASE_DIR, 'base'))
|
||||
: { stmts: 'N/A', branch: 'N/A', funcs: 'N/A', lines: 'N/A' };
|
||||
|
||||
// ── Read test results ──
|
||||
let total = 0, passed = 0, failed = 0, skipped = 0, suites = 0, duration = '0s';
|
||||
let skippedTests = [];
|
||||
try {
|
||||
const files = require('child_process')
|
||||
.execSync(`find "${path.join(temp, 'test-reports')}" -name test-results.json -type f`, { encoding: 'utf8' })
|
||||
.trim().split('\n').filter(Boolean);
|
||||
if (files.length) {
|
||||
const r = JSON.parse(fs.readFileSync(files[0], 'utf8'));
|
||||
total = r.numTotalTests || 0;
|
||||
passed = r.numPassedTests || 0;
|
||||
failed = r.numFailedTests || 0;
|
||||
skipped = r.numPendingTests || 0;
|
||||
suites = r.numTotalTestSuites || 0;
|
||||
const durS = Math.floor((Math.max(...r.testResults.map(t => t.endTime)) - r.startTime) / 1000);
|
||||
duration = durS >= 60 ? `${Math.floor(durS / 60)}m ${durS % 60}s` : `${durS}s`;
|
||||
// Collect skipped test names
|
||||
for (const suite of r.testResults) {
|
||||
for (const t of (suite.assertionResults || [])) {
|
||||
if (t.status === 'pending' || t.status === 'skipped') {
|
||||
skippedTests.push(`- ${t.ancestorTitles.join(' > ')} > ${t.title}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// ── Coverage delta ──
|
||||
function delta(pct, basePct) {
|
||||
if (pct === 'N/A' || basePct === 'N/A') return '—';
|
||||
const d = (pct - basePct).toFixed(1);
|
||||
if (d > 0) return `📈 +${d}%`;
|
||||
if (d < 0) return `📉 ${d}%`;
|
||||
return '=';
|
||||
}
|
||||
|
||||
// ── Build markdown ──
|
||||
const { PR_NUMBER, QUALITY, TESTS, UBUNTU, WINDOWS, MACOS, RUN_ID, HEAD_SHA } = process.env;
|
||||
const prNumber = parseInt(PR_NUMBER, 10);
|
||||
const overall = (QUALITY === 'success' && TESTS === 'success')
|
||||
? '✅ **All checks passed**' : '❌ **Some checks failed**';
|
||||
const sha = HEAD_SHA.slice(0, 7);
|
||||
|
||||
let body = `## CI Report\n\n${overall}   \`${sha}\`\n\n`;
|
||||
|
||||
body += `### Pipeline\n\n`;
|
||||
body += `| Stage | Status | Ubuntu | Windows | macOS |\n`;
|
||||
body += `|-------|--------|--------|---------|-------|\n`;
|
||||
body += `| Typecheck | ${icon(QUALITY)} \`${QUALITY}\` | — | — | — |\n`;
|
||||
body += `| Tests | ${icon(TESTS)} \`${TESTS}\` | ${icon(UBUNTU)} | ${icon(WINDOWS)} | ${icon(MACOS)} |\n\n`;
|
||||
|
||||
if (total > 0) {
|
||||
body += `### Tests\n\n`;
|
||||
body += `| Metric | Value |\n|--------|-------|\n`;
|
||||
body += `| Total | **${total}** |\n`;
|
||||
body += `| Passed | **${passed}** |\n`;
|
||||
if (failed > 0) body += `| Failed | **${failed}** |\n`;
|
||||
if (skipped > 0) body += `| Skipped | ${skipped} |\n`;
|
||||
body += `| Files | ${suites} |\n`;
|
||||
body += `| Duration | ${duration} |\n\n`;
|
||||
|
||||
if (failed === 0) {
|
||||
body += `✅ All **${passed}** tests passed across **${suites}** files\n`;
|
||||
} else {
|
||||
body += `❌ **${failed}** failed / **${passed}** passed\n`;
|
||||
}
|
||||
|
||||
if (skippedTests.length > 0) {
|
||||
body += `\n<details>\n<summary>${skipped} test(s) skipped</summary>\n\n`;
|
||||
body += skippedTests.join('\n') + '\n\n</details>\n';
|
||||
}
|
||||
body += '\n';
|
||||
}
|
||||
|
||||
if (cov.stmts !== 'N/A') {
|
||||
body += `### Coverage\n\n`;
|
||||
body += `| Metric | Coverage | Covered | Base (main) | Delta |\n`;
|
||||
body += `|--------|----------|---------|-------------|-------|\n`;
|
||||
body += `| Statements | **${cov.stmts}%** | ${cov.stmtsCov} | ${base.stmts}% | ${delta(cov.stmts, base.stmts)} |\n`;
|
||||
body += `| Branches | **${cov.branch}%** | ${cov.branchCov} | ${base.branch}% | ${delta(cov.branch, base.branch)} |\n`;
|
||||
body += `| Functions | **${cov.funcs}%** | ${cov.funcsCov} | ${base.funcs}% | ${delta(cov.funcs, base.funcs)} |\n`;
|
||||
body += `| Lines | **${cov.lines}%** | ${cov.linesCov} | ${base.lines}% | ${delta(cov.lines, base.lines)} |\n\n`;
|
||||
} else {
|
||||
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${RUN_ID}`;
|
||||
body += `### Coverage\n\n⚠️ Coverage data unavailable — check the [test job](${runUrl}) for details.\n\n`;
|
||||
}
|
||||
|
||||
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${RUN_ID}`;
|
||||
body += `---\n<sub>📋 [Full run](${runUrl}) · Coverage from Ubuntu · Generated by CI</sub>`;
|
||||
|
||||
// ── Post sticky comment ──
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const marker = '<!-- ci-report -->';
|
||||
const existing = comments.find(c => c.body?.includes(marker));
|
||||
const fullBody = marker + '\n' + body;
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body: fullBody,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: fullBody,
|
||||
});
|
||||
}
|
||||
269
.github/workflows/ci.yml
vendored
269
.github/workflows/ci.yml
vendored
|
|
@ -17,9 +17,7 @@ concurrency:
|
|||
# Each concern lives in its own workflow file for maintainability:
|
||||
# ci-quality.yml — typecheck (tsc --noEmit)
|
||||
# ci-tests.yml — all tests with coverage (ubuntu) + cross-platform
|
||||
#
|
||||
# The PR report runs inline (not via workflow_run) so it uses the
|
||||
# PR branch's code instead of main's — avoids stale report templates.
|
||||
# ci-report.yml — PR comment (workflow_run trigger for fork write access)
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
|
|
@ -55,260 +53,31 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# ── PR Report ────────────────────────────────────────────────────
|
||||
# Posts a sticky comment with test results, coverage, and
|
||||
# per-platform status. Runs inline so it uses the PR branch's
|
||||
# report template (not main's stale version via workflow_run).
|
||||
pr-report:
|
||||
name: PR Report
|
||||
# ── PR metadata for ci-report.yml ────────────────────────────────
|
||||
# Saves PR number and job results so the workflow_run-triggered
|
||||
# report can post comments with a write token (works for forks).
|
||||
save-pr-meta:
|
||||
name: Save PR Metadata
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
needs: [quality, tests]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
pull-requests: write
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Download test reports
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: test-reports
|
||||
path: ${{ runner.temp }}/test-reports
|
||||
continue-on-error: true
|
||||
|
||||
- name: Fetch cross-platform job results
|
||||
id: jobs
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const jobs = await github.rest.actions.listJobsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.runId,
|
||||
per_page: 50,
|
||||
});
|
||||
|
||||
const results = {};
|
||||
for (const job of jobs.data.jobs) {
|
||||
if (job.name.includes('ubuntu')) results.ubuntu = job.conclusion || 'pending';
|
||||
else if (job.name.includes('windows')) results.windows = job.conclusion || 'pending';
|
||||
else if (job.name.includes('macos')) results.macos = job.conclusion || 'pending';
|
||||
}
|
||||
core.setOutput('ubuntu', results.ubuntu || 'unknown');
|
||||
core.setOutput('windows', results.windows || 'unknown');
|
||||
core.setOutput('macos', results.macos || 'unknown');
|
||||
|
||||
- name: Fetch base branch coverage
|
||||
id: base-coverage
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'ci.yml',
|
||||
branch: 'main',
|
||||
status: 'success',
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
if (runs.data.workflow_runs.length === 0) {
|
||||
core.setOutput('found', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
const mainRunId = runs.data.workflow_runs[0].id;
|
||||
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: mainRunId,
|
||||
});
|
||||
|
||||
const testReports = artifacts.data.artifacts.find(a => a.name === 'test-reports');
|
||||
if (!testReports) {
|
||||
core.setOutput('found', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: testReports.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
|
||||
const dest = path.join(process.env.RUNNER_TEMP, 'base-coverage');
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
fs.writeFileSync(path.join(dest, 'base.zip'), Buffer.from(zip.data));
|
||||
core.setOutput('found', 'true');
|
||||
core.setOutput('dir', dest);
|
||||
|
||||
- name: Extract base coverage
|
||||
if: steps.base-coverage.outputs.found == 'true'
|
||||
- name: Write PR metadata
|
||||
shell: bash
|
||||
run: |
|
||||
cd "${{ steps.base-coverage.outputs.dir }}"
|
||||
unzip -o base.zip -d base
|
||||
|
||||
- name: Build and post report
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
QUALITY: ${{ needs.quality.result }}
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
UBUNTU: ${{ steps.jobs.outputs.ubuntu }}
|
||||
WINDOWS: ${{ steps.jobs.outputs.windows }}
|
||||
MACOS: ${{ steps.jobs.outputs.macos }}
|
||||
BASE_FOUND: ${{ steps.base-coverage.outputs.found }}
|
||||
BASE_DIR: ${{ steps.base-coverage.outputs.dir }}
|
||||
run: |
|
||||
mkdir -p pr-meta
|
||||
echo "$PR_NUMBER" > pr-meta/pr-number
|
||||
echo "$QUALITY" > pr-meta/quality-result
|
||||
echo "$TESTS" > pr-meta/tests-result
|
||||
|
||||
- name: Upload PR metadata
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const icon = (s) => ({ success: '✅', failure: '❌', cancelled: '⏭️' }[s] || '❓');
|
||||
const temp = process.env.RUNNER_TEMP;
|
||||
|
||||
// ── Read coverage ──
|
||||
function readCov(dir) {
|
||||
const out = { stmts: 'N/A', branch: 'N/A', funcs: 'N/A', lines: 'N/A',
|
||||
stmtsCov: '', branchCov: '', funcsCov: '', linesCov: '' };
|
||||
try {
|
||||
const files = require('child_process')
|
||||
.execSync(`find "${dir}" -name coverage-summary.json -type f`, { encoding: 'utf8' })
|
||||
.trim().split('\n').filter(Boolean);
|
||||
if (!files.length) return out;
|
||||
const d = JSON.parse(fs.readFileSync(files[0], 'utf8')).total;
|
||||
out.stmts = d.statements.pct; out.branch = d.branches.pct;
|
||||
out.funcs = d.functions.pct; out.lines = d.lines.pct;
|
||||
out.stmtsCov = `${d.statements.covered}/${d.statements.total}`;
|
||||
out.branchCov = `${d.branches.covered}/${d.branches.total}`;
|
||||
out.funcsCov = `${d.functions.covered}/${d.functions.total}`;
|
||||
out.linesCov = `${d.lines.covered}/${d.lines.total}`;
|
||||
} catch {}
|
||||
return out;
|
||||
}
|
||||
|
||||
const cov = readCov(path.join(temp, 'test-reports'));
|
||||
const base = process.env.BASE_FOUND === 'true'
|
||||
? readCov(path.join(process.env.BASE_DIR, 'base'))
|
||||
: { stmts: 'N/A', branch: 'N/A', funcs: 'N/A', lines: 'N/A' };
|
||||
|
||||
// ── Read test results ──
|
||||
let total = 0, passed = 0, failed = 0, skipped = 0, suites = 0, duration = '0s';
|
||||
let skippedTests = [];
|
||||
try {
|
||||
const files = require('child_process')
|
||||
.execSync(`find "${path.join(temp, 'test-reports')}" -name test-results.json -type f`, { encoding: 'utf8' })
|
||||
.trim().split('\n').filter(Boolean);
|
||||
if (files.length) {
|
||||
const r = JSON.parse(fs.readFileSync(files[0], 'utf8'));
|
||||
total = r.numTotalTests || 0;
|
||||
passed = r.numPassedTests || 0;
|
||||
failed = r.numFailedTests || 0;
|
||||
skipped = r.numPendingTests || 0;
|
||||
suites = r.numTotalTestSuites || 0;
|
||||
const durS = Math.floor((Math.max(...r.testResults.map(t => t.endTime)) - r.startTime) / 1000);
|
||||
duration = durS >= 60 ? `${Math.floor(durS / 60)}m ${durS % 60}s` : `${durS}s`;
|
||||
// Collect skipped test names
|
||||
for (const suite of r.testResults) {
|
||||
for (const t of (suite.assertionResults || [])) {
|
||||
if (t.status === 'pending' || t.status === 'skipped') {
|
||||
skippedTests.push(`- ${t.ancestorTitles.join(' > ')} > ${t.title}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// ── Coverage delta ──
|
||||
function delta(pct, basePct) {
|
||||
if (pct === 'N/A' || basePct === 'N/A') return '—';
|
||||
const d = (pct - basePct).toFixed(1);
|
||||
if (d > 0) return `📈 +${d}%`;
|
||||
if (d < 0) return `📉 ${d}%`;
|
||||
return '=';
|
||||
}
|
||||
|
||||
// ── Build markdown ──
|
||||
const { QUALITY, TESTS, UBUNTU, WINDOWS, MACOS } = process.env;
|
||||
const overall = (QUALITY === 'success' && TESTS === 'success')
|
||||
? '✅ **All checks passed**' : '❌ **Some checks failed**';
|
||||
const sha = context.sha.slice(0, 7);
|
||||
|
||||
let body = `## CI Report\n\n${overall}   \`${sha}\`\n\n`;
|
||||
|
||||
body += `### Pipeline\n\n`;
|
||||
body += `| Stage | Status | Ubuntu | Windows | macOS |\n`;
|
||||
body += `|-------|--------|--------|---------|-------|\n`;
|
||||
body += `| Typecheck | ${icon(QUALITY)} \`${QUALITY}\` | — | — | — |\n`;
|
||||
body += `| Tests | ${icon(TESTS)} \`${TESTS}\` | ${icon(UBUNTU)} | ${icon(WINDOWS)} | ${icon(MACOS)} |\n\n`;
|
||||
|
||||
if (total > 0) {
|
||||
body += `### Tests\n\n`;
|
||||
body += `| Metric | Value |\n|--------|-------|\n`;
|
||||
body += `| Total | **${total}** |\n`;
|
||||
body += `| Passed | **${passed}** |\n`;
|
||||
if (failed > 0) body += `| Failed | **${failed}** |\n`;
|
||||
if (skipped > 0) body += `| Skipped | ${skipped} |\n`;
|
||||
body += `| Files | ${suites} |\n`;
|
||||
body += `| Duration | ${duration} |\n\n`;
|
||||
|
||||
if (failed === 0) {
|
||||
body += `✅ All **${passed}** tests passed across **${suites}** files\n`;
|
||||
} else {
|
||||
body += `❌ **${failed}** failed / **${passed}** passed\n`;
|
||||
}
|
||||
|
||||
if (skippedTests.length > 0) {
|
||||
body += `\n<details>\n<summary>${skipped} test(s) skipped</summary>\n\n`;
|
||||
body += skippedTests.join('\n') + '\n\n</details>\n';
|
||||
}
|
||||
body += '\n';
|
||||
}
|
||||
|
||||
if (cov.stmts !== 'N/A') {
|
||||
body += `### Coverage\n\n`;
|
||||
body += `| Metric | Coverage | Covered | Base (main) | Delta |\n`;
|
||||
body += `|--------|----------|---------|-------------|-------|\n`;
|
||||
body += `| Statements | **${cov.stmts}%** | ${cov.stmtsCov} | ${base.stmts}% | ${delta(cov.stmts, base.stmts)} |\n`;
|
||||
body += `| Branches | **${cov.branch}%** | ${cov.branchCov} | ${base.branch}% | ${delta(cov.branch, base.branch)} |\n`;
|
||||
body += `| Functions | **${cov.funcs}%** | ${cov.funcsCov} | ${base.funcs}% | ${delta(cov.funcs, base.funcs)} |\n`;
|
||||
body += `| Lines | **${cov.lines}%** | ${cov.linesCov} | ${base.lines}% | ${delta(cov.lines, base.lines)} |\n\n`;
|
||||
} else {
|
||||
body += `### Coverage\n\n⚠️ Coverage data unavailable — check the [test job](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for details.\n\n`;
|
||||
}
|
||||
|
||||
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
body += `---\n<sub>📋 [Full run](${runUrl}) · Coverage from Ubuntu · Generated by CI</sub>`;
|
||||
|
||||
// ── Post sticky comment ──
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const marker = '<!-- ci-report -->';
|
||||
const existing = comments.find(c => c.body?.includes(marker));
|
||||
const fullBody = marker + '\n' + body;
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body: fullBody,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: fullBody,
|
||||
});
|
||||
}
|
||||
name: pr-meta
|
||||
path: pr-meta/
|
||||
retention-days: 1
|
||||
|
|
|
|||
9
.github/workflows/claude-code-review.yml
vendored
9
.github/workflows/claude-code-review.yml
vendored
|
|
@ -2,9 +2,10 @@ name: Claude Code Review
|
|||
|
||||
# Uses pull_request_target so the workflow runs as defined on the default branch,
|
||||
# which allows access to secrets for posting review comments on fork PRs.
|
||||
# SECURITY: The checkout uses the fork's repo/branch directly so the action
|
||||
# can find the ref. The claude-code-action sandboxes execution — it does NOT
|
||||
# run arbitrary code from the checked-out source.
|
||||
# SECURITY: The checkout pins the fork's HEAD SHA (not the branch name) to
|
||||
# prevent TOCTOU races (force-push between trigger and checkout). The
|
||||
# claude-code-action sandboxes execution — it does NOT run arbitrary code
|
||||
# from the checked-out source.
|
||||
|
||||
on:
|
||||
# Trigger only when explicitly requested:
|
||||
|
|
@ -78,7 +79,7 @@ jobs:
|
|||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
repository: ${{ steps.pr.outputs.repo }}
|
||||
ref: ${{ steps.pr.outputs.branch }}
|
||||
ref: ${{ steps.pr.outputs.sha }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
|
|
|
|||
2
.github/workflows/claude.yml
vendored
2
.github/workflows/claude.yml
vendored
|
|
@ -69,7 +69,7 @@ jobs:
|
|||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
repository: ${{ steps.pr.outputs.is_pr == 'true' && steps.pr.outputs.repo || github.repository }}
|
||||
ref: ${{ steps.pr.outputs.is_pr == 'true' && steps.pr.outputs.branch || '' }}
|
||||
ref: ${{ steps.pr.outputs.is_pr == 'true' && steps.pr.outputs.sha || '' }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue