mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* fix(ci): stop CI Report dying silently when the tests job fails
The "Build report" step in ci-report.yml runs under
`bash --noprofile --norc -e -o pipefail`. It located its inputs with
UNIT_SUMMARY=$(find "$DIR/test-reports" -name ... 2>/dev/null | head -1)
`coverage-merge` in ci-tests.yml is `needs: tests` with no `if: always()`,
so any failing shard skips it and the `test-reports` artifact is never
uploaded. `find` then runs against a directory that does not exist and
exits 1; `-o pipefail` carries that status through `| head -1`, the
command substitution hands it to the assignment, and `-e` kills the step.
The death is invisible: `2>/dev/null` discards find's error and the whole
report is built into `$GITHUB_OUTPUT`, so the step logs nothing and just
reports "Process completed with exit code 1". "Comment on PR" is then
skipped, so the CI Report workflow fails and posts nothing on exactly the
PRs whose tests failed — when the report is most useful. The
"Coverage data unavailable" fallback already existed for this case but
was unreachable, because the script died ~160 lines before it.
Route the four lookups through a `find_first` helper that returns empty
when the root is absent. Verified by extracting the step body and running
it against both artifact layouts: with `test-reports` present the output
is byte-identical to the previous script (1335 bytes), and with it absent
the step now exits 0 and emits the coverage-unavailable report instead of
exiting 1 with an empty $GITHUB_OUTPUT.
Observed on 32 of the last 100 failed runs; correlation with the tests
job's conclusion was 6/6 failure and 4/4 success in the sampled runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(ci): let the prebuild assertion report a missing .node
`Build prebuild` deletes `$pkgdir/prebuilds` before running prebuildify,
so a run that emits nothing without failing leaves `find` searching a path
that no longer exists. Under the step's `shell: bash` (`-e -o pipefail`)
that `find` exits 1 and kills the step before the `test -n "$out"` guard
below it — the guard written to explain exactly this case never runs, and
the job dies with a bare "Process completed with exit code 1".
Same shape as the `ci-report.yml` fix in this PR: a lookup that exits
non-zero on an absent root pre-empts the fallback beneath it. `|| true`
hands the empty result to the guard, which still fails the build, now with
`::error::prebuildify produced no .node`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
460 lines
20 KiB
YAML
460 lines
20 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
|
||
|
||
# Concurrency convention: see CONTRIBUTING.md → "GitHub Actions — Concurrency Convention".
|
||
# Serialize sticky-comment writes per PR so two rapid CI completions don't race.
|
||
# Internal PRs surface in `pull_requests[0].number`. Fork PRs leave that array empty,
|
||
# so we fall back to `<head-repo-full-name>/<head-branch>`, which is stable across
|
||
# reruns and subsequent pushes for the same fork PR (unlike `workflow_run.id` which
|
||
# is unique per run and therefore does not serialize anything).
|
||
concurrency:
|
||
group: ${{ github.workflow }}-${{ github.event.workflow_run.pull_requests[0].number || format('{0}/{1}', github.event.workflow_run.head_repository.full_name, github.event.workflow_run.head_branch) }}
|
||
cancel-in-progress: false
|
||
|
||
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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # 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'));
|
||
|
||
- 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=$(tr -d '[:space:]' < "$DIR/pr_number")
|
||
if ! [[ "$PR_NUM" =~ ^[0-9]+$ ]]; then
|
||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||
echo "::error::Invalid PR number in artifact: '$PR_NUM'"
|
||
exit 0
|
||
fi
|
||
|
||
# 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=$(tr -d '[:space:]' < "$1")
|
||
case "$val" in
|
||
success|failure|cancelled|skipped) echo "$val" ;;
|
||
*) echo "unknown" ;;
|
||
esac
|
||
}
|
||
|
||
{
|
||
echo "skip=false"
|
||
echo "pr_number=$PR_NUM"
|
||
echo "quality=$(validate_result "$DIR/quality_result")"
|
||
echo "tests=$(validate_result "$DIR/tests_result")"
|
||
echo "e2e=$(validate_result "$DIR/e2e_result")"
|
||
} >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Checkout (for vitest config)
|
||
if: steps.meta.outputs.skip != 'true'
|
||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||
with:
|
||
sparse-checkout: gitnexus/vitest.config.ts
|
||
sparse-checkout-cone-mode: false
|
||
|
||
# ── Fetch base branch coverage for delta reporting ───────────
|
||
- name: Fetch base branch coverage
|
||
if: steps.meta.outputs.skip != 'true'
|
||
id: base-coverage
|
||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v7
|
||
with:
|
||
script: |
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// Find recent successful CI runs on main (check several in case
|
||
// the most recent artifact has expired).
|
||
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: 5,
|
||
});
|
||
|
||
if (runs.data.workflow_runs.length === 0) {
|
||
core.setOutput('found', 'false');
|
||
core.info('No successful main branch CI runs found');
|
||
return;
|
||
}
|
||
|
||
// Try each run until we find a downloadable test-reports artifact
|
||
for (const run of runs.data.workflow_runs) {
|
||
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
run_id: run.id,
|
||
});
|
||
|
||
const testReports = artifacts.data.artifacts.find(a => a.name === 'test-reports');
|
||
if (!testReports) {
|
||
core.info(`Run ${run.id}: no test-reports artifact, trying next`);
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
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);
|
||
return;
|
||
} catch (err) {
|
||
// 410 Gone means the artifact expired; try the next run
|
||
if (err.status === 410 || err.response?.status === 410) {
|
||
core.info(`Run ${run.id}: artifact expired, trying next`);
|
||
continue;
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
// All attempts exhausted — no usable base coverage
|
||
core.setOutput('found', 'false');
|
||
core.info('No downloadable test-reports artifact found on main (all expired or missing)');
|
||
|
||
- name: Extract base coverage
|
||
if: steps.meta.outputs.skip != 'true' && steps.base-coverage.outputs.found == 'true'
|
||
shell: bash
|
||
run: |
|
||
cd "${{ steps.base-coverage.outputs.dir }}"
|
||
mkdir -p base
|
||
unzip -o base.zip -d base
|
||
|
||
- name: Build report
|
||
if: steps.meta.outputs.skip != 'true'
|
||
id: report
|
||
shell: bash
|
||
env:
|
||
QUALITY: ${{ steps.meta.outputs.quality }}
|
||
TESTS: ${{ steps.meta.outputs.tests }}
|
||
E2E: ${{ steps.meta.outputs.e2e }}
|
||
BASE_FOUND: ${{ steps.base-coverage.outputs.found }}
|
||
BASE_DIR: ${{ steps.base-coverage.outputs.dir }}
|
||
RUN_URL: ${{ github.event.workflow_run.html_url }}
|
||
run: |
|
||
DIR="$RUNNER_TEMP/artifacts"
|
||
|
||
# ── Helper: read coverage summary into prefixed vars ──
|
||
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 0
|
||
fi
|
||
}
|
||
|
||
# ── Helper: first matching file, tolerating an absent root ──
|
||
# `coverage-merge` (ci-tests.yml) is `needs: tests` with no
|
||
# `if: always()`, so a failing shard skips it and the `test-reports`
|
||
# artifact is never uploaded. A bare `find` on the missing directory
|
||
# exits 1; `-o pipefail` carries that through `| head -1` and `-e`
|
||
# then killed this step — silently, because stderr is discarded and
|
||
# stdout is redirected to $GITHUB_OUTPUT. That skipped "Comment on
|
||
# PR" and failed the run precisely when a PR had failing tests, which
|
||
# is when the report matters most. Degrade to "" instead so the
|
||
# coverage-unavailable fallback below can do its job.
|
||
find_first() {
|
||
local root=$1 name=$2
|
||
[ -d "$root" ] || return 0
|
||
find "$root" -name "$name" -type f 2>/dev/null | head -1 || true
|
||
}
|
||
|
||
# ── Read coverage reports ──
|
||
UNIT_SUMMARY=$(find_first "$DIR/test-reports" "coverage-summary.json")
|
||
|
||
read_cov "U" "$UNIT_SUMMARY"
|
||
|
||
# ── Read base branch coverage (main) ──
|
||
BASE_SUMMARY=""
|
||
if [ "$BASE_FOUND" = "true" ] && [ -n "$BASE_DIR" ]; then
|
||
BASE_SUMMARY=$(find_first "$BASE_DIR/base" "coverage-summary.json")
|
||
fi
|
||
read_cov "B" "$BASE_SUMMARY"
|
||
|
||
# ── Locate test results ──
|
||
RESULTS_FILE=$(find_first "$DIR/test-reports" "test-results.json")
|
||
WEB_RESULTS_FILE=$(find_first "$DIR/test-reports" "web-test-results.json")
|
||
|
||
sum_results() {
|
||
local file=$1
|
||
if [ -n "$file" ] && [ -f "$file" ]; then
|
||
jq -r '"\(.numTotalTests) \(.numPassedTests) \(.numFailedTests) \(.numPendingTests) \(.numTotalTestSuites) \(((.testResults | map(.endTime) | max) - (.startTime)) / 1000 | floor)"' "$file" 2>/dev/null || echo "0 0 0 0 0 0"
|
||
else
|
||
echo "0 0 0 0 0 0"
|
||
fi
|
||
}
|
||
|
||
# `_` placeholder for the suite-count column — positional
|
||
# readability for sum_results' 6-field output, but the value
|
||
# isn't surfaced in the report (suites are tracked per-test
|
||
# framework, not as a top-line metric).
|
||
read -r CLI_T CLI_P CLI_F CLI_S _ CLI_D <<< "$(sum_results "$RESULTS_FILE")"
|
||
read -r WEB_T WEB_P WEB_F WEB_S _ WEB_D <<< "$(sum_results "$WEB_RESULTS_FILE")"
|
||
|
||
TOTAL=$((CLI_T + WEB_T))
|
||
PASSED=$((CLI_P + WEB_P))
|
||
FAILED=$((CLI_F + WEB_F))
|
||
SKIPPED=$((CLI_S + WEB_S))
|
||
DURATION=$((CLI_D > WEB_D ? CLI_D : WEB_D))
|
||
|
||
# ── Status helpers ──
|
||
status_icon() {
|
||
case "$1" in
|
||
success) echo "✅" ;;
|
||
failure) echo "❌" ;;
|
||
cancelled) echo "⏭️" ;;
|
||
*) echo "❓" ;;
|
||
esac
|
||
}
|
||
|
||
# Validate a value looks like a number (integer or decimal, optional
|
||
# leading minus). Returns 1 for anything else — guards against awk
|
||
# injection when artifact values come from untrusted fork code.
|
||
is_numeric() { [[ "$1" =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; }
|
||
|
||
cov_delta() {
|
||
local pct=$1 base=$2
|
||
if [ "$pct" = "N/A" ] || [ "$base" = "N/A" ]; then echo "—"; return; fi
|
||
if ! is_numeric "$pct" || ! is_numeric "$base"; then echo "—"; return; fi
|
||
local diff
|
||
diff=$(awk -v p="$pct" -v b="$base" 'BEGIN { printf "%.1f", p - b }')
|
||
if [ "$(awk -v p="$pct" -v b="$base" 'BEGIN { print (p > b) ? 1 : 0 }')" = "1" ]; then
|
||
echo "📈 +${diff}"
|
||
elif [ "$(awk -v p="$pct" -v b="$base" 'BEGIN { print (p < b) ? 1 : 0 }')" = "1" ]; then
|
||
echo "📉 ${diff}"
|
||
else
|
||
echo "= ${diff}"
|
||
fi
|
||
}
|
||
|
||
cov_bar() {
|
||
local pct=$1 base=$2
|
||
if [ "$pct" = "N/A" ] || ! is_numeric "$pct"; then echo "—"; return; fi
|
||
local filled
|
||
filled=$(awk -v p="$pct" 'BEGIN { printf "%d", p / 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
|
||
# Green if >= base (or base unavailable), red if dropped
|
||
if [ "$base" = "N/A" ] || ! is_numeric "$base" || [ "$(awk -v p="$pct" -v b="$base" 'BEGIN { print (p >= b) ? 1 : 0 }')" = "1" ]; then
|
||
echo "🟢 ${bar}"
|
||
else
|
||
echo "🔴 ${bar}"
|
||
fi
|
||
}
|
||
|
||
# ── Overall status ──
|
||
if [[ "$QUALITY" == "success" && "$TESTS" == "success" && ("$E2E" == "success" || "$E2E" == "skipped") ]]; 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 "$TESTS") Tests | \`${TESTS}\` | unit tests, 3 platforms |"
|
||
echo "| $(status_icon "$E2E") E2E | \`${E2E}\` | gitnexus-web changes only |"
|
||
echo ""
|
||
|
||
if [ "$TOTAL" -gt 0 ] 2>/dev/null; then
|
||
echo "### Test Results"
|
||
echo ""
|
||
echo "| Tests | Passed | Failed | Skipped | Duration |"
|
||
echo "|-------|--------|--------|---------|----------|"
|
||
echo "| ${TOTAL} | ${PASSED} | ${FAILED} | ${SKIPPED} | ${DURATION}s |"
|
||
echo ""
|
||
|
||
if [ "$FAILED" = "0" ]; then
|
||
echo "✅ All **${PASSED}** tests passed"
|
||
else
|
||
echo "❌ **${FAILED}** failed / **${PASSED}** passed"
|
||
fi
|
||
if [ "$SKIPPED" != "0" ]; then
|
||
echo ""
|
||
echo "<details>"
|
||
echo "<summary>${SKIPPED} test(s) skipped — expand for details</summary>"
|
||
echo ""
|
||
for rf in "$RESULTS_FILE" "$WEB_RESULTS_FILE"; do
|
||
if [ -n "$rf" ] && [ -f "$rf" ]; then
|
||
jq -r '
|
||
.testResults[]
|
||
| .assertionResults[]?
|
||
| select(.status == "pending" or .status == "skipped")
|
||
| "- \(.ancestorTitles | join(" > ")) > \(.title)"
|
||
' "$rf" 2>/dev/null || true
|
||
fi
|
||
done
|
||
echo ""
|
||
echo "</details>"
|
||
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 bs=$1 bb=$2 bf=$3 bl=$4
|
||
echo "#### ${label}"
|
||
echo ""
|
||
echo "| Metric | Coverage | Covered | Base | Delta | Status |"
|
||
echo "|--------|----------|---------|------|-------|--------|"
|
||
echo "| Statements | **${s}%** | ${sc} | ${bs}% | $(cov_delta "$s" "$bs") | $(cov_bar "$s" "$bs") |"
|
||
echo "| Branches | **${b}%** | ${bc} | ${bb}% | $(cov_delta "$b" "$bb") | $(cov_bar "$b" "$bb") |"
|
||
echo "| Functions | **${f}%** | ${fc} | ${bf}% | $(cov_delta "$f" "$bf") | $(cov_bar "$f" "$bf") |"
|
||
echo "| Lines | **${l}%** | ${lc} | ${bl}% | $(cov_delta "$l" "$bl") | $(cov_bar "$l" "$bl") |"
|
||
echo ""
|
||
}
|
||
|
||
if [ "$U_STMTS" != "N/A" ]; then
|
||
echo "### Code Coverage"
|
||
echo ""
|
||
cov_table "Tests" \
|
||
"$U_STMTS" "$U_BRANCH" "$U_FUNCS" "$U_LINES" \
|
||
"$U_STMTS_COV" "$U_BRANCH_COV" "$U_FUNCS_COV" "$U_LINES_COV" \
|
||
"$B_STMTS" "$B_BRANCH" "$B_FUNCS" "$B_LINES"
|
||
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@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v2
|
||
with:
|
||
header: ci-report
|
||
number: ${{ steps.meta.outputs.pr_number }}
|
||
message: ${{ steps.report.outputs.body }}
|