From 49709bfe7602cc6b5ad2888660a5992455f19a4c Mon Sep 17 00:00:00 2001
From: Prabal Patra <163229512+alienx5499@users.noreply.github.com>
Date: Mon, 30 Mar 2026 04:04:08 +0530
Subject: [PATCH 1/3] Potential fix for code scanning alert no. 45: Incomplete
string escaping or encoding
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
---
SortVision/tests/quality-assurance.mjs | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/SortVision/tests/quality-assurance.mjs b/SortVision/tests/quality-assurance.mjs
index 56976c4..4a04584 100644
--- a/SortVision/tests/quality-assurance.mjs
+++ b/SortVision/tests/quality-assurance.mjs
@@ -158,8 +158,13 @@ async function writePrCommentReport({ duration, grade }) {
if (failedTestDetails.length > 0) {
lines.push('Failed tests (first 15)
', '', '| Test | Details |', '|------|---------|');
for (const { name, details } of failedTestDetails.slice(0, 15)) {
- const safeName = String(name).replace(/\|/g, '\\|');
- const safeDetails = String(details || '').replace(/\|/g, '\\|').slice(0, 200);
+ const safeName = String(name)
+ .replace(/\\/g, '\\\\')
+ .replace(/\|/g, '\\|');
+ const safeDetails = String(details || '')
+ .replace(/\\/g, '\\\\')
+ .replace(/\|/g, '\\|')
+ .slice(0, 200);
lines.push(`| ${safeName} | ${safeDetails} |`);
}
lines.push('', ' ', '');
From f9335eb28d0d65729658ba90215490b87d3ed2d3 Mon Sep 17 00:00:00 2001
From: alienx5499
Date: Mon, 30 Mar 2026 05:04:47 +0530
Subject: [PATCH 2/3] test(qa): align languages with middleware and language
selector
---
.github/workflows/continuous-integration.yml | 2 +
SortVision/lighthouserc.desktop.json | 8 ----
SortVision/lighthouserc.json | 8 ----
SortVision/tests/quality-assurance.mjs | 41 +++++++-------------
4 files changed, 17 insertions(+), 42 deletions(-)
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index b2b7298..e1f28a8 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -227,6 +227,7 @@ jobs:
http://localhost:3000/es
http://localhost:3000/contributions/overview
uploadArtifacts: true
+ artifactName: lighthouse-results-mobile
temporaryPublicStorage: true
- name: Lighthouse (desktop)
@@ -242,6 +243,7 @@ jobs:
http://localhost:3000/es
http://localhost:3000/contributions/overview
uploadArtifacts: true
+ artifactName: lighthouse-results-desktop
temporaryPublicStorage: true
- name: Print Lighthouse scores (job summary)
diff --git a/SortVision/lighthouserc.desktop.json b/SortVision/lighthouserc.desktop.json
index 6782414..3d0626b 100644
--- a/SortVision/lighthouserc.desktop.json
+++ b/SortVision/lighthouserc.desktop.json
@@ -5,14 +5,6 @@
"settings": {
"preset": "desktop"
}
- },
- "assert": {
- "assertions": {
- "categories:accessibility": ["warn", { "minScore": 0.85 }],
- "categories:seo": ["warn", { "minScore": 0.85 }],
- "categories:best-practices": ["warn", { "minScore": 0.8 }],
- "categories:performance": ["warn", { "minScore": 0.75 }]
- }
}
}
}
diff --git a/SortVision/lighthouserc.json b/SortVision/lighthouserc.json
index 10dfc79..437be75 100644
--- a/SortVision/lighthouserc.json
+++ b/SortVision/lighthouserc.json
@@ -2,14 +2,6 @@
"ci": {
"collect": {
"numberOfRuns": 1
- },
- "assert": {
- "assertions": {
- "categories:accessibility": ["warn", { "minScore": 0.85 }],
- "categories:seo": ["warn", { "minScore": 0.85 }],
- "categories:best-practices": ["warn", { "minScore": 0.8 }],
- "categories:performance": ["warn", { "minScore": 0.75 }]
- }
}
}
}
diff --git a/SortVision/tests/quality-assurance.mjs b/SortVision/tests/quality-assurance.mjs
index 4a04584..5333ed4 100644
--- a/SortVision/tests/quality-assurance.mjs
+++ b/SortVision/tests/quality-assurance.mjs
@@ -73,7 +73,8 @@ const PERF_THRESHOLDS = {
// Test configuration
const ALGORITHMS = ['bubble', 'insertion', 'selection', 'merge', 'quick', 'heap', 'radix', 'bucket'];
-const LANGUAGES = ['en', 'es', 'hi', 'fr', 'de', 'zh', 'ja', 'pt'];
+// Must match `middleware.js` supportedLanguages and the in-app language selector (no Portuguese).
+const LANGUAGES = ['en', 'zh', 'hi', 'es', 'bn', 'fr', 'de', 'ja'];
const TABS = ['config', 'details', 'metrics'];
// Results tracking
@@ -523,16 +524,20 @@ async function testURL(url, expectedStatus, options = {}) {
// Quick validation (30 tests)
async function runQuickValidation() {
- logSection('Quick Validation Suite (30 Tests)');
+ logSection('Quick Validation Suite (~32 tests)');
const tests = [];
// Core pages
tests.push(testURL(`${BASE_URL}/`, 200, { name: 'Homepage', checkSEO: true, checkContent: true }));
tests.push(testURL(`${BASE_URL}/en`, 200, { name: 'English homepage', checkSEO: true }));
+ tests.push(testURL(`${BASE_URL}/zh`, 200, { name: 'Chinese homepage', checkSEO: true }));
+ tests.push(testURL(`${BASE_URL}/hi`, 200, { name: 'Hindi homepage', checkSEO: true }));
tests.push(testURL(`${BASE_URL}/es`, 200, { name: 'Spanish homepage', checkSEO: true }));
+ tests.push(testURL(`${BASE_URL}/bn`, 200, { name: 'Bengali homepage', checkSEO: true }));
tests.push(testURL(`${BASE_URL}/fr`, 200, { name: 'French homepage', checkSEO: true }));
tests.push(testURL(`${BASE_URL}/de`, 200, { name: 'German homepage', checkSEO: true }));
+ tests.push(testURL(`${BASE_URL}/ja`, 200, { name: 'Japanese homepage', checkSEO: true }));
// Algorithm pages with canonicals
tests.push(testURL(`${BASE_URL}/algorithms/config/bubble`, 200, {
@@ -563,11 +568,10 @@ async function runQuickValidation() {
const canonical = lang === 'en' ?
`${CANONICAL_BASE}/algorithms/config/${algo}` :
`${CANONICAL_BASE}/${lang}/algorithms/config/${algo}`;
- const skipCanonical = lang === 'pt'; // PT not fully supported
-
+
tests.push(testURL(`${BASE_URL}/${lang}/algorithms/${tab}/${algo}`, 200, {
name: `${lang.toUpperCase()}/${algo}/${tab}`,
- checkCanonical: !skipCanonical,
+ checkCanonical: true,
canonicalUrl: canonical
}));
}
@@ -606,15 +610,13 @@ async function runComprehensiveValidation() {
for (const algo of ALGORITHMS) {
for (const tab of TABS) {
// English uses /algorithms/config/algo (no /en prefix as it's default)
- // Portuguese not fully supported yet, skip canonical check
- const canonicalUrl = lang === 'en' ?
+ const canonicalUrl = lang === 'en' ?
`${CANONICAL_BASE}/algorithms/config/${algo}` :
`${CANONICAL_BASE}/${lang}/algorithms/config/${algo}`;
- const skipCanonical = lang === 'pt'; // PT not fully supported
-
+
tests.push(testURL(`${BASE_URL}/${lang}/algorithms/${tab}/${algo}`, 200, {
name: `${lang}/${algo}/${tab}`,
- checkCanonical: !skipCanonical,
+ checkCanonical: true,
canonicalUrl: canonicalUrl
}));
}
@@ -662,11 +664,10 @@ async function runIntegrationSuite() {
const canonicalUrl = lang === 'en' ?
`${CANONICAL_BASE}/algorithms/config/${algo}` :
`${CANONICAL_BASE}/${lang}/algorithms/config/${algo}`;
- const skipCanonical = lang === 'pt';
-
+
tests.push(testURL(`${BASE_URL}/${lang}/algorithms/config/${algo}`, 200, {
name: `SEO: ${lang}/${algo}`,
- checkCanonical: !skipCanonical,
+ checkCanonical: true,
canonicalUrl: canonicalUrl,
checkSEO: true,
checkContent: true
@@ -776,18 +777,6 @@ async function runProductionTests() {
tests.push(testURL(`${BASE_URL}/manifest.json`, 200, { name: 'Prod: Manifest' }));
for (const lang of LANGUAGES) {
- // PT is not fully supported in production yet; it may redirect.
- if (lang === 'pt') {
- tests.push(
- testURL(`${BASE_URL}/${lang}`, [200, 301, 308], {
- name: 'Prod: pt (redirect ok)',
- checkSEO: false,
- checkContent: false,
- })
- );
- continue;
- }
-
tests.push(
testURL(`${BASE_URL}/${lang}`, 200, {
name: `Prod: ${lang}`,
@@ -808,7 +797,7 @@ async function runProductionTests() {
}
}
- for (const lang of ['es', 'fr', 'de', 'zh']) {
+ for (const lang of LANGUAGES.filter((l) => l !== 'en')) {
for (const algo of ALGORITHMS) {
tests.push(testURL(`${BASE_URL}/${lang}/algorithms/config/${algo}`, 200, {
name: `Prod: ${lang}/${algo}`,
From 7ad5900f8d55cf7c71d08caa13e91d010553826e Mon Sep 17 00:00:00 2001
From: alienx5499
Date: Mon, 30 Mar 2026 05:22:05 +0530
Subject: [PATCH 3/3] chore(ci): consolidate formatting and linting jobs into a
single step, enhance Lighthouse reporting with matrix strategy for mobile and
desktop, and improve summary handling in CI workflows
---
.github/workflows/README.md | 8 +-
.github/workflows/continuous-integration.yml | 141 ++++++++++++-------
SortVision/scripts/lighthouse-ci-summary.cjs | 77 +++++++---
3 files changed, 157 insertions(+), 69 deletions(-)
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
index 1b8b6af..bf8de7a 100644
--- a/.github/workflows/README.md
+++ b/.github/workflows/README.md
@@ -10,12 +10,12 @@ Workflows are split by responsibility. **Node.js 24** is used in CI to match [`S
| Job | Purpose |
|-----|---------|
-| **Formatting** | Prettier (`pnpm run format:check`) |
-| **Lint** | ESLint |
+| **Format and lint** | Prettier (`pnpm run format:check`) then ESLint (one setup; runs **in parallel** with **Build**). |
| **Build** | Single `pnpm run build`; uploads `SortVision/.next` as artifact `next-build` (no duplicate builds downstream). |
| **Test** | After **Build**: `pnpm install` + restore `next-build` tarball, **`next start`**, `pnpm test`, PR QA comment + `qa-pr-comment` artifact. Runs **in parallel** with **Lighthouse**. |
-| **Lighthouse** | After **Build**: install + download `next-build`, **`next start`**, mobile + desktop Lighthouse ([`lighthouserc.json`](../../SortVision/lighthouserc.json), [`lighthouserc.desktop.json`](../../SortVision/lighthouserc.desktop.json)); job summary via [`lighthouse-ci-summary.cjs`](../../SortVision/scripts/lighthouse-ci-summary.cjs). |
-| **Production validation** | On `main` / `master` only, after **Test** and **Lighthouse**: production smoke tests and HTTP checks |
+| **Lighthouse** | After **Build**: install + download `next-build`, **`next start`**, mobile + desktop Lighthouse ([`lighthouserc.json`](../../SortVision/lighthouserc.json), [`lighthouserc.desktop.json`](../../SortVision/lighthouserc.desktop.json)); uploads `lighthouse-manifest-{mobile,desktop}` (manifest after the treosh action) for the summary job. |
+| **Lighthouse summary** | Merges mobile/desktop manifests, **job summary** + **PR comment** (same pattern as QA), gates Lighthouse. |
+| **Production validation** | On `main` / `master` only, after **Test** and **Lighthouse** (+ summary): production smoke tests and HTTP checks |
Shared setup: [`setup-sortvision`](../actions/setup-sortvision/action.yml) (pnpm, Node, `pnpm install`). Consumer jobs use [`restore-next-build`](../actions/restore-next-build/action.yml) after **Build** to unpack `next-build.tar.gz` into `SortVision/.next`.
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index e1f28a8..4533c0f 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -37,8 +37,9 @@ defaults:
working-directory: ./SortVision
jobs:
- formatting:
- name: Formatting
+ # One checkout + install for both checks (faster than separate Formatting + Lint jobs).
+ format-lint:
+ name: Format and lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
@@ -53,26 +54,12 @@ jobs:
- name: Prettier (check)
run: pnpm run format:check
- lint:
- name: Lint
- runs-on: ubuntu-latest
- timeout-minutes: 15
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (node24 runtime; fewer forced-runtime warnings)
-
- - name: Setup SortVision
- uses: ./.github/actions/setup-sortvision
- with:
- node-version: ${{ env.NODE_VERSION }}
-
- name: ESLint
run: pnpm run lint
- # Single `next build`; consumers run in parallel with `next start` + tests or Lighthouse.
+ # Runs in parallel with format-lint so wall time is max(static, build), not sum (update branch rules if you required separate "Formatting" / "Lint" checks).
build:
name: Build
- needs: [formatting, lint]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
@@ -190,11 +177,22 @@ jobs:
path: SortVision/.qa-pr-comment/
include-hidden-files: true
+ # Mobile + desktop run in parallel (matrix); combined table + gate in lighthouse-summary.
lighthouse:
- name: Lighthouse
+ name: Lighthouse (${{ matrix.variant }})
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 20
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - variant: mobile
+ config: lighthouserc.json
+ artifact: lighthouse-results-mobile
+ - variant: desktop
+ config: lighthouserc.desktop.json
+ artifact: lighthouse-results-desktop
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (node24 runtime; fewer forced-runtime warnings)
@@ -214,12 +212,10 @@ jobs:
npx wait-on http://localhost:3000 --timeout 120000
sleep 2
- - name: Lighthouse (mobile)
- id: lighthouse-mobile
- continue-on-error: true
+ - name: Lighthouse (${{ matrix.variant }})
uses: treosh/lighthouse-ci-action@3e7e23fb74242897f95c0ba9cabad3d0227b9b18 # v12
with:
- configPath: ./SortVision/lighthouserc.json
+ configPath: ./SortVision/${{ matrix.config }}
runs: 1
urls: |
http://localhost:3000
@@ -227,40 +223,91 @@ jobs:
http://localhost:3000/es
http://localhost:3000/contributions/overview
uploadArtifacts: true
- artifactName: lighthouse-results-mobile
+ artifactName: ${{ matrix.artifact }}
temporaryPublicStorage: true
- - name: Lighthouse (desktop)
- id: lighthouse-desktop
- continue-on-error: true
- uses: treosh/lighthouse-ci-action@3e7e23fb74242897f95c0ba9cabad3d0227b9b18 # v12
+ # treosh uploads GitHub artifacts before `lhci upload --target=filesystem` writes manifest.json, so the zip
+ # from uploadArtifacts does not include it. Upload manifest after the action for lighthouse-summary / PR comment.
+ - name: Upload Lighthouse manifest (for summary)
+ uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
- configPath: ./SortVision/lighthouserc.desktop.json
- runs: 1
- urls: |
- http://localhost:3000
- http://localhost:3000/algorithms/config/bubble
- http://localhost:3000/es
- http://localhost:3000/contributions/overview
- uploadArtifacts: true
- artifactName: lighthouse-results-desktop
- temporaryPublicStorage: true
+ name: lighthouse-manifest-${{ matrix.variant }}
+ path: .lighthouseci/manifest.json
+ if-no-files-found: error
+ retention-days: 7
- - name: Print Lighthouse scores (job summary)
- if: always()
+ lighthouse-summary:
+ name: Lighthouse summary
+ needs: [lighthouse]
+ # always(): run after matrix success or failure (still need manifest download + gate); skip if matrix never ran.
+ if: ${{ always() && !cancelled() && needs.lighthouse.result != 'skipped' }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (node24 runtime; fewer forced-runtime warnings)
+
+ - name: Download Lighthouse manifests (mobile)
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ with:
+ name: lighthouse-manifest-mobile
+ path: SortVision/lighthouse-mobile
+ if-no-files-found: ignore
+
+ - name: Download Lighthouse manifests (desktop)
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ with:
+ name: lighthouse-manifest-desktop
+ path: SortVision/lighthouse-desktop
+ if-no-files-found: ignore
+
+ - name: Print Lighthouse scores (job summary + PR comment file)
+ working-directory: SortVision
env:
- MANIFEST_MOBILE: ${{ steps.lighthouse-mobile.outputs.manifest }}
- MANIFEST_DESKTOP: ${{ steps.lighthouse-desktop.outputs.manifest }}
+ MANIFEST_MOBILE_PATH: ${{ github.workspace }}/SortVision/lighthouse-mobile/manifest.json
+ MANIFEST_DESKTOP_PATH: ${{ github.workspace }}/SortVision/lighthouse-desktop/manifest.json
+ LIGHTHOUSE_COMMENT_DIR: ${{ github.event.pull_request && format('{0}/SortVision/.lighthouse-pr-comment', github.workspace) || '' }}
run: node scripts/lighthouse-ci-summary.cjs
+ - name: Prepare Lighthouse PR comment (fallback)
+ if: github.event_name == 'pull_request' && always()
+ run: |
+ DIR="${GITHUB_WORKSPACE}/SortVision/.lighthouse-pr-comment"
+ mkdir -p "$DIR"
+ if [ ! -s "$DIR/comment.md" ]; then
+ {
+ echo ''
+ echo '### Lighthouse (CI)'
+ echo ''
+ echo '**Result:** Score table was not written (early failure). See **Lighthouse summary** job logs.'
+ echo ''
+ echo "[View run](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
+ } > "$DIR/comment.md"
+ fi
+
+ - name: Find previous Lighthouse comment
+ id: find_lighthouse_comment
+ if: github.event_name == 'pull_request' && always()
+ uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ body-includes: ''
+
+ - name: Post Lighthouse report to pull request
+ if: github.event_name == 'pull_request' && always()
+ uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5
+ with:
+ comment-id: ${{ steps.find_lighthouse_comment.outputs.comment-id }}
+ issue-number: ${{ github.event.pull_request.number }}
+ body-path: SortVision/.lighthouse-pr-comment/comment.md
+ edit-mode: replace
+
- name: Require Lighthouse audits to pass
if: always()
run: |
- echo "Lighthouse (mobile): ${{ steps.lighthouse-mobile.outcome }}"
- echo "Lighthouse (desktop): ${{ steps.lighthouse-desktop.outcome }}"
- if [ "${{ steps.lighthouse-mobile.outcome }}" != "success" ] || \
- [ "${{ steps.lighthouse-desktop.outcome }}" != "success" ]; then
- echo "::error::One or more Lighthouse runs failed (see steps above and job summary)."
+ echo "Lighthouse matrix: ${{ needs.lighthouse.result }}"
+ if [ "${{ needs.lighthouse.result }}" != "success" ]; then
+ echo "::error::One or more Lighthouse runs failed (see Lighthouse (mobile) / (desktop) logs and job summary)."
exit 1
fi
@@ -268,7 +315,7 @@ jobs:
name: Production validation
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
- needs: [test, lighthouse]
+ needs: [test, lighthouse, lighthouse-summary]
timeout-minutes: 10
defaults:
run:
diff --git a/SortVision/scripts/lighthouse-ci-summary.cjs b/SortVision/scripts/lighthouse-ci-summary.cjs
index 2abaa5c..90f0b87 100644
--- a/SortVision/scripts/lighthouse-ci-summary.cjs
+++ b/SortVision/scripts/lighthouse-ci-summary.cjs
@@ -1,37 +1,54 @@
#!/usr/bin/env node
/**
- * Appends Lighthouse category scores to GITHUB_STEP_SUMMARY (mobile + desktop manifests from LHCI action).
+ * Lighthouse category scores: optional GITHUB_STEP_SUMMARY, optional PR comment file (CI).
*/
const fs = require('fs');
+const path = require('path');
-const out = process.env.GITHUB_STEP_SUMMARY;
-if (!out) {
+const summaryPath = process.env.GITHUB_STEP_SUMMARY;
+const commentDir = process.env.LIGHTHOUSE_COMMENT_DIR;
+
+if (!summaryPath && !commentDir) {
process.exit(0);
}
+/** Prefer file path (parallel CI jobs upload manifests); fall back to inline JSON from the same job. */
+function loadManifest(envJsonKey, envPathKey) {
+ const p = process.env[envPathKey];
+ if (p) {
+ try {
+ if (fs.existsSync(p)) {
+ return fs.readFileSync(p, 'utf8');
+ }
+ } catch {
+ /* ignore */
+ }
+ }
+ return process.env[envJsonKey] || '';
+}
+
function pct(n) {
if (n == null || Number.isNaN(n)) return '—';
return `${Math.round(Number(n) * 100)}`;
}
-function table(title, manifestJson) {
+/** Markdown table cell: escape backslashes first, then pipes (predictable table parsing). */
+function escapeMarkdownTableCell(s) {
+ return String(s).replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
+}
+
+function tableMarkdown(title, manifestJson) {
const lines = [`### ${title}`, ''];
let rows;
try {
rows = JSON.parse(manifestJson || '[]');
} catch {
lines.push('_Could not parse Lighthouse manifest._', '');
- const text = lines.join('\n');
- console.log(text);
- fs.appendFileSync(out, text + '\n', 'utf8');
- return;
+ return lines.join('\n');
}
if (!Array.isArray(rows) || rows.length === 0) {
lines.push('_No manifest rows (run may have failed before collection)._', '');
- const text = lines.join('\n');
- console.log(text);
- fs.appendFileSync(out, text + '\n', 'utf8');
- return;
+ return lines.join('\n');
}
lines.push(
@@ -41,17 +58,41 @@ function table(title, manifestJson) {
for (const r of rows) {
const s = r.summary || {};
- const url = String(r.url || '—').replace(/\|/g, '\\|');
+ const url = escapeMarkdownTableCell(r.url || '—');
const bp = s['best-practices'] ?? s.bestPractices;
lines.push(
`| ${url} | ${pct(s.performance)} | ${pct(s.accessibility)} | ${pct(bp)} | ${pct(s.seo)} |`
);
}
lines.push('');
- const text = lines.join('\n');
- console.log(text);
- fs.appendFileSync(out, text + '\n', 'utf8');
+ return lines.join('\n');
+}
+
+function buildReportBody() {
+ const mobile = loadManifest('MANIFEST_MOBILE', 'MANIFEST_MOBILE_PATH');
+ const desktop = loadManifest('MANIFEST_DESKTOP', 'MANIFEST_DESKTOP_PATH');
+ return [tableMarkdown('Lighthouse (mobile)', mobile), tableMarkdown('Lighthouse (desktop)', desktop)].join(
+ '\n'
+ );
}
-table('Lighthouse (mobile)', process.env.MANIFEST_MOBILE);
-table('Lighthouse (desktop)', process.env.MANIFEST_DESKTOP);
+const body = buildReportBody();
+console.log(body);
+
+if (summaryPath) {
+ fs.appendFileSync(summaryPath, `${body}\n`, 'utf8');
+}
+
+if (commentDir) {
+ fs.mkdirSync(commentDir, { recursive: true });
+ const runUrl =
+ process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID
+ ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
+ : '';
+ const headerLines = ['', '### Lighthouse (CI)', ''];
+ if (runUrl) {
+ headerLines.push(`[View workflow run](${runUrl})`, '');
+ }
+ const text = `${headerLines.join('\n')}${body}\n`;
+ fs.writeFileSync(path.join(commentDir, 'comment.md'), text, 'utf8');
+}