mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
* feat: upgrade @ladybugdb/core to 0.15.2 and remove segfault workarounds
The upstream fix (ladybug-nodejs#1) resolves the child QueryResult lifetime
segfault, making .close() safe on all platforms. This removes 6 workaround
sites:
- Remove `dangerouslyIgnoreUnhandledErrors` from vitest config
- Remove platform-conditional .close() guards in global-setup and test helper
- Delete test/setup.ts (process._getActiveHandles unref hack)
- Replace no-op cleanup in test-indexed-db.ts with real adapter close
- Fix pool adapter closeOne() to properly close connections with shared
Database refcount guard and orphaned connection handling in checkin()
- Update segfault-related comments across the codebase
Also bumps @ladybugdb/wasm-core to ^0.15.2 in gitnexus-web for consistency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: keep dangerouslyIgnoreUnhandledErrors for macOS N-API exit crash
The N-API destructor ordering crash during worker fork exit on macOS is
independent of the QueryResult lifetime fix in 0.15.2. Tests pass, but
the exit triggers a crash. Keep the flag with an updated comment
explaining the actual cause. Can be removed once LadybugDB fixes all
destructor ordering issues upstream.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ci: unify test run for single-pass coverage
- Update `npm test` to run all tests (unit + integration + lbug-db)
via `vitest run` instead of `vitest run test/unit`
- Add `test:unit` script for running unit tests only
- Remove `ci-integration.yml` — the per-file lbug-db process isolation
is no longer needed with `dangerouslyIgnoreUnhandledErrors` and
`fileParallelism: false` handling fork exit issues
- Update `ci-unit-tests.yml` to run all tests with build + coverage
- Simplify `ci.yml` gate (two jobs: quality + tests)
- Simplify `ci-report.yml` (single coverage artifact, no merge step)
* fix: update cli-commands test for renamed test:all → test:unit script
* fix: set USERPROFILE in setup-skills test for Windows compatibility
os.homedir() checks USERPROFILE on Windows, not HOME.
* fix: add isolate: false to lbug-db project to prevent fork crashes
On macOS, N-API destructors crash fork workers on exit. With
isolate: true (default), vitest recycles the fork between files,
triggering the crash after each file. After several crashes, the
remaining lbug-db files never execute.
isolate: false keeps all 8 lbug-db files in a single fork — the
fork only exits once after all files complete, and that single exit
crash is caught by dangerouslyIgnoreUnhandledErrors.
* fix: add unique sequence.groupOrder to vitest projects
Vitest v4 requires unique groupOrder when projects have different
maxWorkers (lbug-db has fileParallelism: false → maxWorkers: 1).
* fix: await async close() in global-setup and remove isolate: false
global-setup.ts called conn.close() and db.close() without await —
these return Promise<void> in @ladybugdb/core 0.15.2. The setup
function returned before the DB was fully closed, so vitest forks
hit a stale file lock when opening the same DB path, crashing the
lbug-db worker before any test ran.
isolate: false caused native state corruption after 2-3 open/close
cycles in the same fork (vitest-specific, not reproducible in plain
Node.js). Without it, each file gets its own module scope and the
N-API destructor crash at fork exit is caught by
dangerouslyIgnoreUnhandledErrors.
Also fixes fire-and-forget close() calls in the pool adapter —
try/catch around an async close() never catches rejections; changed
to .catch(() => {}) for proper unhandled-rejection prevention.
Before: 0/8 lbug-db files ran on macOS CI (fork crash).
After: 8/8 pass, 84 files, 3077 tests, zero errors.
* fix: update project index references in AGENTS.md and CLAUDE.md to reflect correct symbol counts and relationships
* feat: enhance lbug adapter with external database support and write operation validation
* feat: create ci-tests workflow for comprehensive test coverage across platforms
* ci: move PR report inline to ci.yml, delete ci-report.yml
The old ci-report.yml used workflow_run which always runs code from
the default branch (main). This meant the PR comment used main's
stale report template that still referenced the old unit/integration
split architecture — causing "Merge coverage reports" failures.
Moving the report inline to ci.yml means it runs from the PR branch
and uses the current report template. The report now shows:
- per-platform status (Ubuntu/Windows/macOS columns)
- unified test counts from the single vitest run
- coverage with base branch (main) delta comparison
- commit SHA for traceability
Also removes the save-pr-meta job since the report no longer needs
a separate workflow_run trigger.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
314 lines
13 KiB
YAML
314 lines
13 KiB
YAML
name: CI
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
paths-ignore: ['**.md', 'docs/**', 'LICENSE']
|
||
pull_request:
|
||
branches: [main]
|
||
paths-ignore: ['**.md', 'docs/**', 'LICENSE']
|
||
workflow_call:
|
||
|
||
concurrency:
|
||
group: ci-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
# ── Reusable workflow orchestration ─────────────────────────────────
|
||
# 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.
|
||
|
||
jobs:
|
||
quality:
|
||
uses: ./.github/workflows/ci-quality.yml
|
||
permissions:
|
||
contents: read
|
||
|
||
tests:
|
||
uses: ./.github/workflows/ci-tests.yml
|
||
permissions:
|
||
contents: read
|
||
|
||
# ── Unified CI gate ──────────────────────────────────────────────
|
||
# Single required check for branch protection.
|
||
ci-status:
|
||
name: CI Gate
|
||
needs: [quality, tests]
|
||
if: always()
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
steps:
|
||
- name: Check all jobs passed
|
||
shell: bash
|
||
env:
|
||
QUALITY: ${{ needs.quality.result }}
|
||
TESTS: ${{ needs.tests.result }}
|
||
run: |
|
||
echo "Quality: $QUALITY"
|
||
echo "Tests: $TESTS"
|
||
if [[ "$QUALITY" != "success" ]] ||
|
||
[[ "$TESTS" != "success" ]]; then
|
||
echo "::error::One or more CI jobs failed"
|
||
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
|
||
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'
|
||
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:
|
||
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 }}
|
||
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,
|
||
});
|
||
}
|