mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* Initial plan * fix: add pull-requests write permissions to GitHub Actions workflows Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * fix(ci): remove ineffective job-level permissions from reusable workflow * fix(ci): pass PR write permission from caller to reusable unit-tests workflow * fix(ci): harden CI/CD workflows with security fixes and reliability improvements - Pin all actions to commit SHAs to prevent supply-chain attacks - Fix shell injection in ci-integration.yml by using env vars instead of direct interpolation - Scope permissions per-job in publish.yml (was granting pull-requests:write to publish job) - Restrict claude-code-review to trusted contributors only (OWNER/MEMBER/COLLABORATOR) - Switch claude-code-review to pull_request_target for fork PR support - Fix fail-fast: false in ci-unit-tests.yml cross-platform matrix - Remove duplicate ubuntu-latest from unit test matrix - Add timeouts to all workflow jobs - Improve kuzu-db test loop to continue on failure and report per-file errors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ci): read thresholds from `vitest.config.ts` * fix(ci): move PR report to workflow_run for fork PR support The sticky-pull-request-comment and vitest-coverage-report-action both fail on fork PRs because pull_request events receive a read-only GITHUB_TOKEN. This extracts PR reporting into a separate ci-report.yml workflow triggered by workflow_run, which always gets read/write tokens. Changes: - ci.yml: replace pr-report job with save-pr-meta artifact upload - ci-unit-tests.yml: remove davelosert/vitest-coverage-report-action, add coverage-final.json to artifact for merging - ci-integration.yml: add ubuntu coverage job for non-kuzu groups - ci-report.yml (new): workflow_run handler that downloads artifacts, merges unit + integration coverage via Istanbul, and posts combined PR comment with sticky-pull-request-comment * feat(ci): show unit, integration, and merged coverage in PR report - Disable coverage thresholds for integration-only run (partial coverage) - Display combined coverage as the primary metric - Show per-suite breakdown (unit / integration) in expandable details - Thresholds applied against combined coverage, not individual suites * fix(ci): add coverage collection input for PR reports and validate job results * fix(ci): refine Claude Code Review workflow to support issue comments and enhance trusted contributor checks --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
432 lines
19 KiB
YAML
432 lines
19 KiB
YAML
name: CI Report
|
|
|
|
# Triggered after the CI workflow completes. Because workflow_run
|
|
# always runs code from the *default branch*, it receives a read/write
|
|
# GITHUB_TOKEN — even when the triggering PR comes from a fork.
|
|
|
|
on:
|
|
workflow_run:
|
|
workflows: ["CI"]
|
|
types: [completed]
|
|
|
|
permissions:
|
|
actions: read # needed to list/download workflow run artifacts
|
|
contents: read # needed for sparse checkout of vitest.config.ts
|
|
pull-requests: write # needed to post sticky PR comment
|
|
|
|
jobs:
|
|
pr-report:
|
|
name: PR Report
|
|
# Only run for pull-request CI runs
|
|
if: >-
|
|
github.event.workflow_run.event == 'pull_request' &&
|
|
github.event.workflow_run.conclusion != 'cancelled'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
# ── Download artifacts from the CI run ────────────────────────
|
|
- name: Download artifacts
|
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
|
with:
|
|
script: |
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const runId = context.payload.workflow_run.id;
|
|
|
|
const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
run_id: runId,
|
|
});
|
|
|
|
async function downloadArtifact(name, dest) {
|
|
const match = allArtifacts.data.artifacts.find(a => a.name === name);
|
|
if (!match) {
|
|
core.warning(`Artifact "${name}" not found`);
|
|
return false;
|
|
}
|
|
const zip = await github.rest.actions.downloadArtifact({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
artifact_id: match.id,
|
|
archive_format: 'zip',
|
|
});
|
|
fs.mkdirSync(dest, { recursive: true });
|
|
fs.writeFileSync(path.join(dest, `${name}.zip`), Buffer.from(zip.data));
|
|
return true;
|
|
}
|
|
|
|
const temp = process.env.RUNNER_TEMP;
|
|
await downloadArtifact('pr-meta', path.join(temp, 'dl'));
|
|
await downloadArtifact('test-reports', path.join(temp, 'dl'));
|
|
await downloadArtifact('integration-reports', path.join(temp, 'dl'));
|
|
|
|
- name: Extract artifacts
|
|
shell: bash
|
|
run: |
|
|
cd "$RUNNER_TEMP/dl"
|
|
# Extract each artifact into its own directory to avoid filename collisions
|
|
for z in *.zip; do
|
|
[ -f "$z" ] || continue
|
|
name="${z%.zip}"
|
|
mkdir -p "$RUNNER_TEMP/artifacts/$name"
|
|
unzip -o "$z" -d "$RUNNER_TEMP/artifacts/$name"
|
|
done
|
|
|
|
- name: Read PR metadata
|
|
id: meta
|
|
shell: bash
|
|
run: |
|
|
DIR="$RUNNER_TEMP/artifacts/pr-meta"
|
|
if [ ! -f "$DIR/pr_number" ]; then
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
echo "::warning::pr_number artifact missing — skipping report"
|
|
exit 0
|
|
fi
|
|
|
|
# Validate PR number is a positive integer (artifact comes from
|
|
# untrusted fork code, so treat contents defensively).
|
|
PR_NUM=$(cat "$DIR/pr_number" | tr -d '[:space:]')
|
|
if ! [[ "$PR_NUM" =~ ^[0-9]+$ ]]; then
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
echo "::error::Invalid PR number in artifact: '$PR_NUM'"
|
|
exit 0
|
|
fi
|
|
|
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
echo "pr_number=$PR_NUM" >> "$GITHUB_OUTPUT"
|
|
# Validate job-result strings against known GitHub Actions values.
|
|
# Artifact contents come from the PR workflow (potentially untrusted
|
|
# fork code), so we whitelist to prevent newline injection into
|
|
# GITHUB_OUTPUT.
|
|
validate_result() {
|
|
local val
|
|
val=$(cat "$1" | tr -d '[:space:]')
|
|
case "$val" in
|
|
success|failure|cancelled|skipped) echo "$val" ;;
|
|
*) echo "unknown" ;;
|
|
esac
|
|
}
|
|
|
|
echo "quality=$(validate_result "$DIR/quality_result")" >> "$GITHUB_OUTPUT"
|
|
echo "unit=$(validate_result "$DIR/unit_result")" >> "$GITHUB_OUTPUT"
|
|
echo "integration=$(validate_result "$DIR/integration_result")" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Checkout (for vitest config)
|
|
if: steps.meta.outputs.skip != 'true'
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
with:
|
|
sparse-checkout: gitnexus/vitest.config.ts
|
|
sparse-checkout-cone-mode: false
|
|
|
|
# ── Merge coverage from unit + integration ─────────────────────
|
|
- name: Setup Node.js
|
|
if: steps.meta.outputs.skip != 'true'
|
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
with:
|
|
node-version: 20
|
|
|
|
- name: Install coverage merge tools
|
|
if: steps.meta.outputs.skip != 'true'
|
|
run: npm install --no-save istanbul-lib-coverage istanbul-lib-report istanbul-reports
|
|
|
|
- name: Merge coverage reports
|
|
if: steps.meta.outputs.skip != 'true'
|
|
id: coverage
|
|
shell: bash
|
|
run: |
|
|
DIR="$RUNNER_TEMP/artifacts"
|
|
UNIT_COV=$(find "$DIR/test-reports" -name "coverage-final.json" -type f 2>/dev/null | head -1)
|
|
INTEG_COV=$(find "$DIR/integration-reports" -name "coverage-final.json" -type f 2>/dev/null | head -1)
|
|
MERGED_DIR="$RUNNER_TEMP/merged-coverage"
|
|
mkdir -p "$MERGED_DIR"
|
|
|
|
if [ -n "$UNIT_COV" ] && [ -n "$INTEG_COV" ]; then
|
|
echo "has_merged=true" >> "$GITHUB_OUTPUT"
|
|
# Merge using Node.js + istanbul-lib-coverage.
|
|
# Paths are passed via env vars to avoid shell interpolation
|
|
# inside the script string.
|
|
UNIT_COV_PATH="$UNIT_COV" \
|
|
INTEG_COV_PATH="$INTEG_COV" \
|
|
MERGED_OUT_DIR="$MERGED_DIR" \
|
|
node -e "
|
|
const libCoverage = require('istanbul-lib-coverage');
|
|
const libReport = require('istanbul-lib-report');
|
|
const reports = require('istanbul-reports');
|
|
const fs = require('fs');
|
|
|
|
const map = libCoverage.createCoverageMap({});
|
|
map.merge(JSON.parse(fs.readFileSync(process.env.UNIT_COV_PATH, 'utf8')));
|
|
map.merge(JSON.parse(fs.readFileSync(process.env.INTEG_COV_PATH, 'utf8')));
|
|
|
|
const context = libReport.createContext({
|
|
coverageMap: map,
|
|
dir: process.env.MERGED_OUT_DIR,
|
|
});
|
|
reports.create('json-summary').execute(context);
|
|
console.log('Merged coverage written to ' + process.env.MERGED_OUT_DIR + '/coverage-summary.json');
|
|
"
|
|
elif [ -n "$UNIT_COV" ]; then
|
|
echo "has_merged=false" >> "$GITHUB_OUTPUT"
|
|
echo "::warning::Integration coverage not found — using unit coverage only"
|
|
else
|
|
echo "has_merged=false" >> "$GITHUB_OUTPUT"
|
|
echo "::warning::No coverage data found"
|
|
fi
|
|
|
|
- name: Build report
|
|
if: steps.meta.outputs.skip != 'true'
|
|
id: report
|
|
shell: bash
|
|
env:
|
|
QUALITY: ${{ steps.meta.outputs.quality }}
|
|
UNIT: ${{ steps.meta.outputs.unit }}
|
|
INTEG: ${{ steps.meta.outputs.integration }}
|
|
HAS_MERGED: ${{ steps.coverage.outputs.has_merged }}
|
|
RUN_URL: ${{ github.event.workflow_run.html_url }}
|
|
run: |
|
|
DIR="$RUNNER_TEMP/artifacts"
|
|
MERGED_DIR="$RUNNER_TEMP/merged-coverage"
|
|
|
|
# ── Helper: read coverage summary into prefixed vars ──
|
|
# Uses printf -v for safe variable assignment (no eval).
|
|
read_cov() {
|
|
local prefix=$1 file=$2
|
|
if [ -n "$file" ] && [ -f "$file" ]; then
|
|
local val
|
|
val=$(jq -r '.total.statements.pct // "N/A"' "$file" 2>/dev/null) || val="N/A"
|
|
printf -v "${prefix}_STMTS" '%s' "$val"
|
|
val=$(jq -r '.total.branches.pct // "N/A"' "$file" 2>/dev/null) || val="N/A"
|
|
printf -v "${prefix}_BRANCH" '%s' "$val"
|
|
val=$(jq -r '.total.functions.pct // "N/A"' "$file" 2>/dev/null) || val="N/A"
|
|
printf -v "${prefix}_FUNCS" '%s' "$val"
|
|
val=$(jq -r '.total.lines.pct // "N/A"' "$file" 2>/dev/null) || val="N/A"
|
|
printf -v "${prefix}_LINES" '%s' "$val"
|
|
val=$(jq -r '"\(.total.statements.covered)/\(.total.statements.total)"' "$file" 2>/dev/null) || val=""
|
|
printf -v "${prefix}_STMTS_COV" '%s' "$val"
|
|
val=$(jq -r '"\(.total.branches.covered)/\(.total.branches.total)"' "$file" 2>/dev/null) || val=""
|
|
printf -v "${prefix}_BRANCH_COV" '%s' "$val"
|
|
val=$(jq -r '"\(.total.functions.covered)/\(.total.functions.total)"' "$file" 2>/dev/null) || val=""
|
|
printf -v "${prefix}_FUNCS_COV" '%s' "$val"
|
|
val=$(jq -r '"\(.total.lines.covered)/\(.total.lines.total)"' "$file" 2>/dev/null) || val=""
|
|
printf -v "${prefix}_LINES_COV" '%s' "$val"
|
|
return 0
|
|
else
|
|
printf -v "${prefix}_STMTS" '%s' "N/A"
|
|
printf -v "${prefix}_BRANCH" '%s' "N/A"
|
|
printf -v "${prefix}_FUNCS" '%s' "N/A"
|
|
printf -v "${prefix}_LINES" '%s' "N/A"
|
|
printf -v "${prefix}_STMTS_COV" '%s' ""
|
|
printf -v "${prefix}_BRANCH_COV" '%s' ""
|
|
printf -v "${prefix}_FUNCS_COV" '%s' ""
|
|
printf -v "${prefix}_LINES_COV" '%s' ""
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# ── Read all three coverage reports ──
|
|
UNIT_SUMMARY=$(find "$DIR/test-reports" -name "coverage-summary.json" -type f 2>/dev/null | head -1)
|
|
INTEG_SUMMARY=$(find "$DIR/integration-reports" -name "coverage-summary.json" -type f 2>/dev/null | head -1)
|
|
MERGED_SUMMARY="$MERGED_DIR/coverage-summary.json"
|
|
|
|
read_cov "U" "$UNIT_SUMMARY"
|
|
HAS_UNIT=$?
|
|
read_cov "I" "$INTEG_SUMMARY"
|
|
HAS_INTEG=$?
|
|
read_cov "M" "$MERGED_SUMMARY"
|
|
|
|
# ── Locate test results (unit) ──
|
|
RESULTS_FILE=$(find "$DIR/test-reports" -name "test-results.json" -type f 2>/dev/null | head -1)
|
|
INTEG_RESULTS=$(find "$DIR/integration-reports" -name "integration-results.json" -type f 2>/dev/null | head -1)
|
|
|
|
if [ -n "$RESULTS_FILE" ]; then
|
|
U_TOTAL=$(jq -r '.numTotalTests' "$RESULTS_FILE" 2>/dev/null || echo 0)
|
|
U_PASSED=$(jq -r '.numPassedTests' "$RESULTS_FILE" 2>/dev/null || echo 0)
|
|
U_FAILED=$(jq -r '.numFailedTests' "$RESULTS_FILE" 2>/dev/null || echo 0)
|
|
U_SKIPPED=$(jq -r '.numPendingTests' "$RESULTS_FILE" 2>/dev/null || echo 0)
|
|
U_SUITES=$(jq -r '.numTotalTestSuites' "$RESULTS_FILE" 2>/dev/null || echo 0)
|
|
U_DURATION=$(jq -r '((.testResults | map(.endTime) | max) - (.startTime)) / 1000 | floor' "$RESULTS_FILE" 2>/dev/null || echo 0)
|
|
else
|
|
U_TOTAL=0; U_PASSED=0; U_FAILED=0; U_SKIPPED=0; U_SUITES=0; U_DURATION=0
|
|
fi
|
|
|
|
if [ -n "$INTEG_RESULTS" ]; then
|
|
I_TOTAL=$(jq -r '.numTotalTests' "$INTEG_RESULTS" 2>/dev/null || echo 0)
|
|
I_PASSED=$(jq -r '.numPassedTests' "$INTEG_RESULTS" 2>/dev/null || echo 0)
|
|
I_FAILED=$(jq -r '.numFailedTests' "$INTEG_RESULTS" 2>/dev/null || echo 0)
|
|
I_SKIPPED=$(jq -r '.numPendingTests' "$INTEG_RESULTS" 2>/dev/null || echo 0)
|
|
I_SUITES=$(jq -r '.numTotalTestSuites' "$INTEG_RESULTS" 2>/dev/null || echo 0)
|
|
I_DURATION=$(jq -r '((.testResults | map(.endTime) | max) - (.startTime)) / 1000 | floor' "$INTEG_RESULTS" 2>/dev/null || echo 0)
|
|
else
|
|
I_TOTAL=0; I_PASSED=0; I_FAILED=0; I_SKIPPED=0; I_SUITES=0; I_DURATION=0
|
|
fi
|
|
|
|
# ── Sum test results ──
|
|
TOTAL=$((U_TOTAL + I_TOTAL))
|
|
PASSED=$((U_PASSED + I_PASSED))
|
|
FAILED=$((U_FAILED + I_FAILED))
|
|
SKIPPED=$((U_SKIPPED + I_SKIPPED))
|
|
SUITES=$((U_SUITES + I_SUITES))
|
|
DURATION=$((U_DURATION + I_DURATION))
|
|
|
|
# ── Coverage thresholds (read from vitest.config.ts) ──
|
|
if [ -f gitnexus/vitest.config.ts ]; then
|
|
THRESH_STMTS=$(grep -oP 'statements:\s*\K[0-9]+' gitnexus/vitest.config.ts || echo 0)
|
|
THRESH_BRANCH=$(grep -oP 'branches:\s*\K[0-9]+' gitnexus/vitest.config.ts || echo 0)
|
|
THRESH_FUNCS=$(grep -oP 'functions:\s*\K[0-9]+' gitnexus/vitest.config.ts || echo 0)
|
|
THRESH_LINES=$(grep -oP 'lines:\s*\K[0-9]+' gitnexus/vitest.config.ts || echo 0)
|
|
else
|
|
THRESH_STMTS=0; THRESH_BRANCH=0; THRESH_FUNCS=0; THRESH_LINES=0
|
|
fi
|
|
|
|
# ── Status helpers ──
|
|
status_icon() {
|
|
case "$1" in
|
|
success) echo "✅" ;;
|
|
failure) echo "❌" ;;
|
|
cancelled) echo "⏭️" ;;
|
|
*) echo "❓" ;;
|
|
esac
|
|
}
|
|
|
|
cov_bar() {
|
|
local pct=$1 thresh=$2
|
|
if [ "$pct" = "N/A" ]; then echo "—"; return; fi
|
|
local filled
|
|
filled=$(awk "BEGIN { printf \"%d\", $pct / 5 }")
|
|
(( filled < 0 )) && filled=0
|
|
(( filled > 20 )) && filled=20
|
|
local empty=$((20 - filled))
|
|
local bar=""
|
|
for ((i=0; i<filled; i++)); do bar+="█"; done
|
|
for ((i=0; i<empty; i++)); do bar+="░"; done
|
|
if [ "$(awk "BEGIN { print ($pct >= $thresh) ? 1 : 0 }")" = "1" ]; then
|
|
echo "🟢 ${bar}"
|
|
else
|
|
echo "🔴 ${bar}"
|
|
fi
|
|
}
|
|
|
|
# ── Overall status ──
|
|
if [[ "$QUALITY" == "success" && "$UNIT" == "success" && "$INTEG" == "success" ]]; then
|
|
OVERALL="✅ **All checks passed**"
|
|
else
|
|
OVERALL="❌ **Some checks failed**"
|
|
fi
|
|
|
|
# ── Build markdown ──
|
|
{
|
|
echo "body<<GITNEXUS_CI_REPORT_EOF_7f3a"
|
|
echo "## CI Report"
|
|
echo ""
|
|
echo "${OVERALL}"
|
|
echo ""
|
|
echo "### Pipeline Status"
|
|
echo ""
|
|
echo "| Stage | Status | Details |"
|
|
echo "|-------|--------|---------|"
|
|
echo "| $(status_icon "$QUALITY") Typecheck | \`${QUALITY}\` | tsc --noEmit |"
|
|
echo "| $(status_icon "$UNIT") Unit Tests | \`${UNIT}\` | 3 platforms |"
|
|
echo "| $(status_icon "$INTEG") Integration | \`${INTEG}\` | 3 OS x 4 groups = 12 jobs |"
|
|
echo ""
|
|
|
|
if [ "$TOTAL" -gt 0 ] 2>/dev/null; then
|
|
echo "### Test Results"
|
|
echo ""
|
|
if [ "$FAILED" = "0" ]; then
|
|
echo "✅ **${PASSED}** passed"
|
|
else
|
|
echo "❌ **${FAILED}** failed / **${PASSED}** passed"
|
|
fi
|
|
if [ "$SKIPPED" != "0" ]; then
|
|
echo " · ${SKIPPED} skipped"
|
|
fi
|
|
echo " · ${SUITES} suites · ${TOTAL} total"
|
|
echo " · ⏱️ ${DURATION}s"
|
|
if [ "$I_TOTAL" -gt 0 ] 2>/dev/null; then
|
|
echo " · 📊 ${U_TOTAL} unit + ${I_TOTAL} integration"
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
# ── Coverage table helper ──
|
|
cov_table() {
|
|
local label=$1 s=$2 b=$3 f=$4 l=$5 sc=$6 bc=$7 fc=$8 lc=$9
|
|
shift 9
|
|
local ts=$1 tb=$2 tf=$3 tl=$4
|
|
echo "#### ${label}"
|
|
echo ""
|
|
echo "| Metric | Coverage | Covered | Threshold | Status |"
|
|
echo "|--------|----------|---------|-----------|--------|"
|
|
echo "| Statements | **${s}%** | ${sc} | ${ts}% | $(cov_bar "$s" "$ts") |"
|
|
echo "| Branches | **${b}%** | ${bc} | ${tb}% | $(cov_bar "$b" "$tb") |"
|
|
echo "| Functions | **${f}%** | ${fc} | ${tf}% | $(cov_bar "$f" "$tf") |"
|
|
echo "| Lines | **${l}%** | ${lc} | ${tl}% | $(cov_bar "$l" "$tl") |"
|
|
echo ""
|
|
}
|
|
|
|
if [ "$M_STMTS" != "N/A" ]; then
|
|
echo "### Code Coverage"
|
|
echo ""
|
|
cov_table "Combined (Unit + Integration)" \
|
|
"$M_STMTS" "$M_BRANCH" "$M_FUNCS" "$M_LINES" \
|
|
"$M_STMTS_COV" "$M_BRANCH_COV" "$M_FUNCS_COV" "$M_LINES_COV" \
|
|
"$THRESH_STMTS" "$THRESH_BRANCH" "$THRESH_FUNCS" "$THRESH_LINES"
|
|
|
|
echo "<details>"
|
|
echo "<summary>Coverage breakdown by test suite</summary>"
|
|
echo ""
|
|
if [ "$U_STMTS" != "N/A" ]; then
|
|
cov_table "Unit Tests" \
|
|
"$U_STMTS" "$U_BRANCH" "$U_FUNCS" "$U_LINES" \
|
|
"$U_STMTS_COV" "$U_BRANCH_COV" "$U_FUNCS_COV" "$U_LINES_COV" \
|
|
"$THRESH_STMTS" "$THRESH_BRANCH" "$THRESH_FUNCS" "$THRESH_LINES"
|
|
fi
|
|
if [ "$I_STMTS" != "N/A" ]; then
|
|
cov_table "Integration Tests" \
|
|
"$I_STMTS" "$I_BRANCH" "$I_FUNCS" "$I_LINES" \
|
|
"$I_STMTS_COV" "$I_BRANCH_COV" "$I_FUNCS_COV" "$I_LINES_COV" \
|
|
"$THRESH_STMTS" "$THRESH_BRANCH" "$THRESH_FUNCS" "$THRESH_LINES"
|
|
fi
|
|
echo "</details>"
|
|
echo ""
|
|
echo "<details>"
|
|
echo "<summary>Coverage thresholds are auto-ratcheted — they only go up</summary>"
|
|
echo ""
|
|
echo "Vitest \`thresholds.autoUpdate\` bumps the floor whenever local coverage exceeds it."
|
|
echo "CI enforces the current thresholds; developers commit the ratcheted values."
|
|
echo "</details>"
|
|
echo ""
|
|
elif [ "$U_STMTS" != "N/A" ]; then
|
|
echo "### Code Coverage (Unit only)"
|
|
echo ""
|
|
cov_table "Unit Tests" \
|
|
"$U_STMTS" "$U_BRANCH" "$U_FUNCS" "$U_LINES" \
|
|
"$U_STMTS_COV" "$U_BRANCH_COV" "$U_FUNCS_COV" "$U_LINES_COV" \
|
|
"$THRESH_STMTS" "$THRESH_BRANCH" "$THRESH_FUNCS" "$THRESH_LINES"
|
|
echo "<details>"
|
|
echo "<summary>Coverage thresholds are auto-ratcheted — they only go up</summary>"
|
|
echo ""
|
|
echo "Vitest \`thresholds.autoUpdate\` bumps the floor whenever local coverage exceeds it."
|
|
echo "CI enforces the current thresholds; developers commit the ratcheted values."
|
|
echo "</details>"
|
|
echo ""
|
|
else
|
|
echo "### Code Coverage"
|
|
echo ""
|
|
echo "⚠️ Coverage data unavailable - check the [unit test job](${RUN_URL}) for details."
|
|
echo ""
|
|
fi
|
|
|
|
echo "---"
|
|
echo "<sub>📋 [View full run](${RUN_URL}) · Generated by CI</sub>"
|
|
echo "GITNEXUS_CI_REPORT_EOF_7f3a"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Comment on PR
|
|
if: steps.meta.outputs.skip != 'true'
|
|
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2
|
|
with:
|
|
header: ci-report
|
|
number: ${{ steps.meta.outputs.pr_number }}
|
|
message: ${{ steps.report.outputs.body }}
|