From 1e7a8fb8d9c1af550aaf704127720311809eedf9 Mon Sep 17 00:00:00 2001 From: Vucomir Ianculov Date: Wed, 4 Feb 2026 14:05:09 +0200 Subject: [PATCH 01/12] DEVOPS-1274 added shared workflow for npm --- .github/workflows/check-npm-application.yml | 117 ++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 .github/workflows/check-npm-application.yml diff --git a/.github/workflows/check-npm-application.yml b/.github/workflows/check-npm-application.yml new file mode 100644 index 0000000..0ac3e2d --- /dev/null +++ b/.github/workflows/check-npm-application.yml @@ -0,0 +1,117 @@ +name: Reusable - Check NPM App + +on: + workflow_call: + inputs: + agent: + required: false + type: string + description: "Agent where the workflow will be run" + default: ubuntu-latest + checkout_repository: + required: false + type: string + description: "Repository to checkout (owner/repo format). If empty, uses the current repository" + default: "" + checkout_ref: + required: false + type: string + description: "Git ref to checkout (branch, tag, or commit SHA). If empty, uses the default branch" + default: "" + checkout_depth: + required: false + type: number + description: "Depth of checkout. If empty, uses the default depth" + default: 0 + node_version: + required: false + type: string + default: "24" + cache: + required: false + type: string + default: npm + install_dependencies: + required: false + type: boolean + default: true + build_packages: + required: false + type: boolean + default: true + build_command: + required: false + type: string + default: "" + run_checks: + required: false + type: boolean + default: true + run_lint: + required: false + type: boolean + default: true + lint_command: + required: false + type: string + default: "npm run lint" + run_audit: + required: false + type: boolean + default: true + audit_level: + required: false + type: string + description: "The value of --audit-level flag" + default: "moderate" + working_directory: + required: false + type: string + default: "." + +jobs: + check_app: + runs-on: ${{ inputs.agent }} + defaults: + run: + working-directory: ${{ inputs.working_directory }} + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: ${{ inputs.checkout_depth }} + ref: ${{ inputs.checkout_ref }} + repository: ${{ inputs.checkout_repository }} + + - uses: actions/setup-node@v6 + with: + node-version: ${{ inputs.node_version }} + cache: ${{ inputs.cache }} + + - name: Install dependencies + if: ${{ inputs.install_dependencies }} + run: npm ci + + - name: Build packages + if: ${{ inputs.build_packages }} + run: | + ${{ inputs.build_command }} + + - name: Run linting + if: ${{ inputs.run_lint }} + run: ${{ inputs.lint_command }} + + - name: Run typescript checks + if: ${{ inputs.run_checks }} + run: npm run types:check + + - name: Run npm audit + if: ${{ inputs.run_audit }} + uses: oke-py/npm-audit-action@v3 + with: + audit_level: ${{ inputs.audit_level }} + github_token: ${{ secrets.GITHUB_TOKEN }} + create_issues: false + create_pr_comments: true + working_directory: ${{ inputs.working_directory }} From 7bfa8bb51c65660b3118df079afb84581c186cb4 Mon Sep 17 00:00:00 2001 From: Vucomir Ianculov Date: Wed, 4 Feb 2026 14:17:42 +0200 Subject: [PATCH 02/12] DEVOPS-1274 added shared workflow for npm --- .github/workflows/check-npm-application.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-npm-application.yml b/.github/workflows/check-npm-application.yml index 0ac3e2d..f5cb9da 100644 --- a/.github/workflows/check-npm-application.yml +++ b/.github/workflows/check-npm-application.yml @@ -30,7 +30,8 @@ on: cache: required: false type: string - default: npm + description: "Cache type (npm, yarn, or empty string to disable). Defaults to npm if package-lock.json exists" + default: "npm" install_dependencies: required: false type: boolean @@ -88,6 +89,7 @@ jobs: with: node-version: ${{ inputs.node_version }} cache: ${{ inputs.cache }} + cache-dependency-path: ${{ inputs.working_directory != '.' && format('{0}/package-lock.json', inputs.working_directory) || '' }} - name: Install dependencies if: ${{ inputs.install_dependencies }} From dcfced208eb941324ae5c6b31b9376b114bfcbc3 Mon Sep 17 00:00:00 2001 From: Vucomir Ianculov Date: Wed, 4 Feb 2026 14:19:48 +0200 Subject: [PATCH 03/12] DEVOPS-1274 added shared workflow for npm --- .github/workflows/check-npm-application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-npm-application.yml b/.github/workflows/check-npm-application.yml index f5cb9da..220bb3b 100644 --- a/.github/workflows/check-npm-application.yml +++ b/.github/workflows/check-npm-application.yml @@ -71,7 +71,7 @@ on: default: "." jobs: - check_app: + check: runs-on: ${{ inputs.agent }} defaults: run: From 346ac032a598a61a67d38be52fb73c5ea7d24237 Mon Sep 17 00:00:00 2001 From: Vucomir Ianculov Date: Wed, 4 Feb 2026 14:32:30 +0200 Subject: [PATCH 04/12] DEVOPS-1274 added shared workflow for npm --- .github/workflows/check-npm-application.yml | 105 ++++++++++++++++++-- 1 file changed, 98 insertions(+), 7 deletions(-) diff --git a/.github/workflows/check-npm-application.yml b/.github/workflows/check-npm-application.yml index 220bb3b..7487906 100644 --- a/.github/workflows/check-npm-application.yml +++ b/.github/workflows/check-npm-application.yml @@ -108,12 +108,103 @@ jobs: if: ${{ inputs.run_checks }} run: npm run types:check - - name: Run npm audit + - name: Run npm audit and create markdown report if: ${{ inputs.run_audit }} - uses: oke-py/npm-audit-action@v3 + id: npm-audit + continue-on-error: true + run: | + cd "${{ inputs.working_directory }}" + + # Run npm audit and capture text output + AUDIT_OUTPUT=$(npm audit --audit-level=${{ inputs.audit_level }} 2>&1) || AUDIT_EXIT_CODE=$? + + # Create markdown report file + REPORT_FILE=$(mktemp) + + # Check if vulnerabilities were found + if echo "$AUDIT_OUTPUT" | grep -q "vulnerabilities"; then + # Format as markdown + { + echo "## πŸ“‹ npm audit report" + echo "" + echo "\`\`\`" + echo "$AUDIT_OUTPUT" + echo "\`\`\`" + echo "" + echo "> To fix these issues, run: \`npm audit fix\` or \`npm audit fix --force\` (for breaking changes)" + } > "$REPORT_FILE" + else + # No vulnerabilities found + { + echo "## βœ… npm audit report" + echo "" + echo "No vulnerabilities found." + } > "$REPORT_FILE" + AUDIT_EXIT_CODE=0 + fi + + # Read the report + MARKDOWN_REPORT=$(cat "$REPORT_FILE") + + # Add to workflow summary + cat "$REPORT_FILE" >> $GITHUB_STEP_SUMMARY + + # Store for PR comment (using multiline output) + { + echo "markdown_report<> $GITHUB_OUTPUT + + # Cleanup + rm -f "$REPORT_FILE" + + # Set exit code based on audit result + if [ -n "$AUDIT_EXIT_CODE" ] && [ "$AUDIT_EXIT_CODE" != "0" ]; then + echo "has_vulnerabilities=true" >> $GITHUB_OUTPUT + exit $AUDIT_EXIT_CODE + else + echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT + fi + + - name: Create or update PR comment with audit results + if: ${{ inputs.run_audit && github.event_name == 'pull_request' }} + uses: actions/github-script@v7 with: - audit_level: ${{ inputs.audit_level }} - github_token: ${{ secrets.GITHUB_TOKEN }} - create_issues: false - create_pr_comments: true - working_directory: ${{ inputs.working_directory }} + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const report = `${{ steps.npm-audit.outputs.markdown_report }}`; + + // Check if comment already exists + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existingComment = comments.data.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('npm audit report') + ); + + if (existingComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: report + }); + } else { + // Only create new comment if there are vulnerabilities + // (to avoid cluttering PRs with "no vulnerabilities" comments) + const hasVulnerabilities = '${{ steps.npm-audit.outputs.has_vulnerabilities }}' === 'true'; + if (hasVulnerabilities) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: report + }); + } + } From cf5428baa66786ca27f9109d2cb9b9dd3f346bf3 Mon Sep 17 00:00:00 2001 From: Vucomir Ianculov Date: Wed, 4 Feb 2026 14:36:21 +0200 Subject: [PATCH 05/12] DEVOPS-1274 added shared workflow for npm --- .github/workflows/check-npm-application.yml | 61 +++++++++++++++------ 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/.github/workflows/check-npm-application.yml b/.github/workflows/check-npm-application.yml index 7487906..6903286 100644 --- a/.github/workflows/check-npm-application.yml +++ b/.github/workflows/check-npm-application.yml @@ -113,17 +113,42 @@ jobs: id: npm-audit continue-on-error: true run: | - cd "${{ inputs.working_directory }}" + # Create markdown report file + REPORT_FILE=$(mktemp) # Run npm audit and capture text output AUDIT_OUTPUT=$(npm audit --audit-level=${{ inputs.audit_level }} 2>&1) || AUDIT_EXIT_CODE=$? - # Create markdown report file - REPORT_FILE=$(mktemp) - - # Check if vulnerabilities were found - if echo "$AUDIT_OUTPUT" | grep -q "vulnerabilities"; then - # Format as markdown + # Check if audit command failed (not just vulnerabilities found) + if [ -n "$AUDIT_EXIT_CODE" ] && [ "$AUDIT_EXIT_CODE" != "0" ]; then + # Check if it's because of vulnerabilities or actual error + if echo "$AUDIT_OUTPUT" | grep -q "vulnerabilities"; then + # Vulnerabilities found - format as markdown + { + echo "## πŸ“‹ npm audit report" + echo "" + echo "\`\`\`" + echo "$AUDIT_OUTPUT" + echo "\`\`\`" + echo "" + echo "> To fix these issues, run: \`npm audit fix\` or \`npm audit fix --force\` (for breaking changes)" + } > "$REPORT_FILE" + echo "has_vulnerabilities=true" >> $GITHUB_OUTPUT + else + # Actual error running audit + { + echo "## ⚠️ npm audit error" + echo "" + echo "Failed to run npm audit:" + echo "" + echo "\`\`\`" + echo "$AUDIT_OUTPUT" + echo "\`\`\`" + } > "$REPORT_FILE" + echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT + fi + elif echo "$AUDIT_OUTPUT" | grep -qi "vulnerabilities"; then + # Vulnerabilities found (exit code 0 but output contains vulnerabilities) { echo "## πŸ“‹ npm audit report" echo "" @@ -133,6 +158,7 @@ jobs: echo "" echo "> To fix these issues, run: \`npm audit fix\` or \`npm audit fix --force\` (for breaking changes)" } > "$REPORT_FILE" + echo "has_vulnerabilities=true" >> $GITHUB_OUTPUT else # No vulnerabilities found { @@ -140,12 +166,10 @@ jobs: echo "" echo "No vulnerabilities found." } > "$REPORT_FILE" + echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT AUDIT_EXIT_CODE=0 fi - # Read the report - MARKDOWN_REPORT=$(cat "$REPORT_FILE") - # Add to workflow summary cat "$REPORT_FILE" >> $GITHUB_STEP_SUMMARY @@ -159,22 +183,23 @@ jobs: # Cleanup rm -f "$REPORT_FILE" - # Set exit code based on audit result - if [ -n "$AUDIT_EXIT_CODE" ] && [ "$AUDIT_EXIT_CODE" != "0" ]; then - echo "has_vulnerabilities=true" >> $GITHUB_OUTPUT - exit $AUDIT_EXIT_CODE - else - echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT - fi + # Exit with audit exit code (0 if no vulnerabilities, non-zero if vulnerabilities found) + exit ${AUDIT_EXIT_CODE:-0} - name: Create or update PR comment with audit results - if: ${{ inputs.run_audit && github.event_name == 'pull_request' }} + if: ${{ inputs.run_audit && github.event_name == 'pull_request' && steps.npm-audit.outcome != 'skipped' }} uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const report = `${{ steps.npm-audit.outputs.markdown_report }}`; + // Skip if report is empty + if (!report || report.trim() === '') { + console.log('No audit report available, skipping comment'); + return; + } + // Check if comment already exists const comments = await github.rest.issues.listComments({ owner: context.repo.owner, From f4188508d213b16e770b495841dbb1231a53303a Mon Sep 17 00:00:00 2001 From: Vucomir Ianculov Date: Wed, 4 Feb 2026 14:39:49 +0200 Subject: [PATCH 06/12] DEVOPS-1274 added shared workflow for npm --- .github/workflows/check-npm-application.yml | 59 +++------------------ 1 file changed, 7 insertions(+), 52 deletions(-) diff --git a/.github/workflows/check-npm-application.yml b/.github/workflows/check-npm-application.yml index 6903286..1fde516 100644 --- a/.github/workflows/check-npm-application.yml +++ b/.github/workflows/check-npm-application.yml @@ -173,63 +173,18 @@ jobs: # Add to workflow summary cat "$REPORT_FILE" >> $GITHUB_STEP_SUMMARY - # Store for PR comment (using multiline output) - { - echo "markdown_report<> $GITHUB_OUTPUT - - # Cleanup - rm -f "$REPORT_FILE" + # Save report file path for PR comment step + echo "report_file=$REPORT_FILE" >> $GITHUB_OUTPUT # Exit with audit exit code (0 if no vulnerabilities, non-zero if vulnerabilities found) exit ${AUDIT_EXIT_CODE:-0} - name: Create or update PR comment with audit results if: ${{ inputs.run_audit && github.event_name == 'pull_request' && steps.npm-audit.outcome != 'skipped' }} - uses: actions/github-script@v7 + uses: thollander/actions-comment-pull-request@v3 with: github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const report = `${{ steps.npm-audit.outputs.markdown_report }}`; - - // Skip if report is empty - if (!report || report.trim() === '') { - console.log('No audit report available, skipping comment'); - return; - } - - // Check if comment already exists - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const existingComment = comments.data.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('npm audit report') - ); - - if (existingComment) { - // Update existing comment - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existingComment.id, - body: report - }); - } else { - // Only create new comment if there are vulnerabilities - // (to avoid cluttering PRs with "no vulnerabilities" comments) - const hasVulnerabilities = '${{ steps.npm-audit.outputs.has_vulnerabilities }}' === 'true'; - if (hasVulnerabilities) { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: report - }); - } - } + file-path: ${{ steps.npm-audit.outputs.report_file }} + comment-tag: npm-audit-report + mode: upsert + create-if-not-exists: ${{ steps.npm-audit.outputs.has_vulnerabilities || 'false' }} From a8ad34a0dacd642f6bea1aa77f126fce6f66f02f Mon Sep 17 00:00:00 2001 From: Vucomir Ianculov Date: Wed, 4 Feb 2026 14:47:05 +0200 Subject: [PATCH 07/12] DEVOPS-1274 added shared workflow for npm --- .github/workflows/check-npm-application.yml | 227 +++++++++++++++----- 1 file changed, 174 insertions(+), 53 deletions(-) diff --git a/.github/workflows/check-npm-application.yml b/.github/workflows/check-npm-application.yml index 1fde516..b973108 100644 --- a/.github/workflows/check-npm-application.yml +++ b/.github/workflows/check-npm-application.yml @@ -108,76 +108,197 @@ jobs: if: ${{ inputs.run_checks }} run: npm run types:check - - name: Run npm audit and create markdown report + - name: Run npm audit (JSON) if: ${{ inputs.run_audit }} + id: npm-audit-json + continue-on-error: true + run: | + npm audit --audit-level=${{ inputs.audit_level }} --json > audit.json 2>&1 || AUDIT_EXIT_CODE=$? + echo "exit_code=${AUDIT_EXIT_CODE:-0}" >> $GITHUB_OUTPUT + + - name: Convert npm audit JSON to Markdown + if: ${{ inputs.run_audit && steps.npm-audit-json.outcome != 'skipped' }} id: npm-audit continue-on-error: true run: | - # Create markdown report file REPORT_FILE=$(mktemp) - # Run npm audit and capture text output - AUDIT_OUTPUT=$(npm audit --audit-level=${{ inputs.audit_level }} 2>&1) || AUDIT_EXIT_CODE=$? - - # Check if audit command failed (not just vulnerabilities found) - if [ -n "$AUDIT_EXIT_CODE" ] && [ "$AUDIT_EXIT_CODE" != "0" ]; then - # Check if it's because of vulnerabilities or actual error - if echo "$AUDIT_OUTPUT" | grep -q "vulnerabilities"; then - # Vulnerabilities found - format as markdown - { - echo "## πŸ“‹ npm audit report" - echo "" - echo "\`\`\`" - echo "$AUDIT_OUTPUT" - echo "\`\`\`" - echo "" - echo "> To fix these issues, run: \`npm audit fix\` or \`npm audit fix --force\` (for breaking changes)" - } > "$REPORT_FILE" - echo "has_vulnerabilities=true" >> $GITHUB_OUTPUT - else - # Actual error running audit - { - echo "## ⚠️ npm audit error" - echo "" - echo "Failed to run npm audit:" - echo "" - echo "\`\`\`" - echo "$AUDIT_OUTPUT" - echo "\`\`\`" - } > "$REPORT_FILE" - echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT - fi - elif echo "$AUDIT_OUTPUT" | grep -qi "vulnerabilities"; then - # Vulnerabilities found (exit code 0 but output contains vulnerabilities) + # Check if audit.json exists and is valid JSON + if [ ! -f audit.json ] || ! node -e "JSON.parse(require('fs').readFileSync('audit.json','utf8'))" 2>/dev/null; then + # Fallback to text output if JSON parsing fails + TEXT_OUTPUT=$(npm audit --audit-level=${{ inputs.audit_level }} 2>&1 || true) { - echo "## πŸ“‹ npm audit report" - echo "" - echo "\`\`\`" - echo "$AUDIT_OUTPUT" - echo "\`\`\`" + echo "## ⚠️ npm audit error" echo "" - echo "> To fix these issues, run: \`npm audit fix\` or \`npm audit fix --force\` (for breaking changes)" - } > "$REPORT_FILE" - echo "has_vulnerabilities=true" >> $GITHUB_OUTPUT - else - # No vulnerabilities found - { - echo "## βœ… npm audit report" + echo "Failed to generate audit report:" echo "" - echo "No vulnerabilities found." + echo '```' + echo "$TEXT_OUTPUT" + echo '```' } > "$REPORT_FILE" echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT - AUDIT_EXIT_CODE=0 + echo "report_file=$REPORT_FILE" >> $GITHUB_OUTPUT + cat "$REPORT_FILE" >> $GITHUB_STEP_SUMMARY + exit 0 fi + # Convert JSON to markdown + node <<'NODE' > "$REPORT_FILE" + const fs = require('node:fs'); + + const audit = JSON.parse(fs.readFileSync('audit.json','utf8')); + + // Supports npm v6 ("advisories") and npm v7+ ("vulnerabilities" + auditReportVersion) + function summarize(a) { + // npm v7+ format + if (a.metadata?.vulnerabilities) { + return { + totalDependencies: a.metadata.totalDependencies ?? null, + counts: a.metadata.vulnerabilities + }; + } + // npm v6 format + if (a.metadata && (a.advisories || a.actions)) { + // best-effort: v6 metadata sometimes differs; we'll derive counts from advisories + const adv = Object.values(a.advisories || {}); + const counts = { info:0, low:0, moderate:0, high:0, critical:0 }; + for (const x of adv) counts[x.severity] = (counts[x.severity] || 0) + 1; + return { totalDependencies: a.metadata.totalDependencies ?? null, counts }; + } + // fallback + return { totalDependencies: null, counts: {} }; + } + + const { totalDependencies, counts } = summarize(audit); + + const sevOrder = ['critical','high','moderate','low','info']; + const countOrZero = (k) => Number.isFinite(counts?.[k]) ? counts[k] : 0; + + const totalVulns = sevOrder.reduce((s,k)=>s+countOrZero(k),0); + + const md = []; + md.push(`## πŸ“‹ npm audit report`); + md.push(''); + md.push(`Generated: **${new Date().toISOString()}**`); + md.push(''); + md.push(`### Summary`); + if (totalDependencies != null) md.push(`- Total dependencies: **${totalDependencies}**`); + md.push(`- Total vulnerabilities: **${totalVulns}**`); + md.push(''); + md.push(`| Severity | Count |`); + md.push(`|---|---:|`); + for (const k of sevOrder) md.push(`| ${k} | ${countOrZero(k)} |`); + md.push(''); + + // Details (v7+) + if (audit.vulnerabilities && typeof audit.vulnerabilities === 'object') { + md.push(`### Details`); + md.push(''); + const vulns = Object.entries(audit.vulnerabilities) + .map(([name, v]) => ({ name, ...v })) + .filter(v => v.severity && v.via && v.via.length); + + // Sort by severity then name + const sevRank = { critical:5, high:4, moderate:3, low:2, info:1 }; + vulns.sort((a,b)=> (sevRank[b.severity]||0) - (sevRank[a.severity]||0) || a.name.localeCompare(b.name)); + + for (const v of vulns) { + md.push(`#### ${v.name} β€” ${String(v.severity).toUpperCase()}`); + if (v.range) md.push(`- Vulnerable range: \`${v.range}\``); + + // "via" can contain strings or advisory objects + const advisories = (v.via || []).filter(x => typeof x === 'object'); + const viaStrings = (v.via || []).filter(x => typeof x === 'string'); + + if (viaStrings.length) md.push(`- Via: ${viaStrings.map(s=>`\`${s}\``).join(', ')}`); + + if (advisories.length) { + // Print up to a few advisory lines to keep summary readable + for (const a of advisories.slice(0, 5)) { + md.push(`- ${a.title || 'Advisory'}${a.url ? ` - [View advisory](${a.url})` : ''}`); + if (a.cwe?.length) md.push(` - CWE: ${a.cwe.join(', ')}`); + if (a.cvss?.score != null) md.push(` - CVSS: ${a.cvss.score}`); + } + if (advisories.length > 5) md.push(`- …and ${advisories.length - 5} more advisories`); + } + + if (Array.isArray(v.nodes) && v.nodes.length) { + md.push(`- Nodes: ${v.nodes.slice(0,10).map(n=>`\`${n}\``).join(', ')}${v.nodes.length>10 ? ' …' : ''}`); + } + if (Array.isArray(v.effects) && v.effects.length) { + md.push(`- Affects: ${v.effects.map(e=>`\`${e}\``).join(', ')}`); + } + if (v.fixAvailable) { + if (v.fixAvailable === true) md.push(`- Fix available: βœ…`); + else if (v.fixAvailable.isSemVerMajor) { + md.push(`- Fix available: βœ… \`npm audit fix --force\` ⚠️ **Breaking change**`); + } else { + md.push(`- Fix available: βœ… \`npm audit fix\``); + } + } else { + md.push(`- Fix available: ❌`); + } + md.push(''); + } + } + + // Details (v6) + else if (audit.advisories && typeof audit.advisories === 'object') { + md.push(`### Details`); + md.push(''); + const adv = Object.values(audit.advisories); + const sevRank = { critical:5, high:4, moderate:3, low:2 }; + adv.sort((a,b)=> (sevRank[b.severity]||0) - (sevRank[a.severity]||0) || a.module_name.localeCompare(b.module_name)); + + for (const a of adv) { + md.push(`#### ${a.module_name} β€” ${String(a.severity).toUpperCase()}`); + md.push(`- Title: ${a.title}`); + md.push(`- Vulnerable: \`${a.vulnerable_versions}\``); + md.push(`- Patched: \`${a.patched_versions || 'n/a'}\``); + if (a.url) md.push(`- Advisory: [${a.url}](${a.url})`); + md.push(''); + } + } + + // Add fix instructions if vulnerabilities found + if (totalVulns > 0) { + md.push(`> πŸ’‘ To fix these issues, run: \`npm audit fix\` or \`npm audit fix --force\` (for breaking changes)`); + } else { + md.push(`> βœ… No vulnerabilities found.`); + } + + process.stdout.write(md.join('\n')); + + // Emit vulnerability count for later steps + const critical = countOrZero('critical'); + const high = countOrZero('high'); + const moderate = countOrZero('moderate'); + const low = countOrZero('low'); + const info = countOrZero('info'); + const hasVulns = totalVulns > 0; + + const outputFile = process.env.GITHUB_OUTPUT || 'ghout.txt'; + require('fs').appendFileSync(outputFile, `has_vulnerabilities=${hasVulns}\n`); + require('fs').appendFileSync(outputFile, `critical_count=${critical}\n`); + require('fs').appendFileSync(outputFile, `high_count=${high}\n`); + require('fs').appendFileSync(outputFile, `moderate_count=${moderate}\n`); + require('fs').appendFileSync(outputFile, `low_count=${low}\n`); + require('fs').appendFileSync(outputFile, `info_count=${info}\n`); + require('fs').appendFileSync(outputFile, `total_vulnerabilities=${totalVulns}\n`); + NODE + + echo "report_file=$REPORT_FILE" >> $GITHUB_OUTPUT + # Add to workflow summary cat "$REPORT_FILE" >> $GITHUB_STEP_SUMMARY - # Save report file path for PR comment step - echo "report_file=$REPORT_FILE" >> $GITHUB_OUTPUT + # Cleanup + rm -f audit.json - # Exit with audit exit code (0 if no vulnerabilities, non-zero if vulnerabilities found) - exit ${AUDIT_EXIT_CODE:-0} + # Exit with appropriate code + if [ "${{ steps.npm-audit-json.outputs.exit_code }}" != "0" ]; then + exit ${{ steps.npm-audit-json.outputs.exit_code }} + fi - name: Create or update PR comment with audit results if: ${{ inputs.run_audit && github.event_name == 'pull_request' && steps.npm-audit.outcome != 'skipped' }} From 2725327df21f113de0a8c84d4785206dbc4a4955 Mon Sep 17 00:00:00 2001 From: Vucomir Ianculov Date: Wed, 4 Feb 2026 14:50:25 +0200 Subject: [PATCH 08/12] DEVOPS-1274 added shared workflow for npm --- .github/workflows/check-npm-application.yml | 87 +++++++++++++-------- 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/.github/workflows/check-npm-application.yml b/.github/workflows/check-npm-application.yml index b973108..189bb3e 100644 --- a/.github/workflows/check-npm-application.yml +++ b/.github/workflows/check-npm-application.yml @@ -202,44 +202,58 @@ jobs: const sevRank = { critical:5, high:4, moderate:3, low:2, info:1 }; vulns.sort((a,b)=> (sevRank[b.severity]||0) - (sevRank[a.severity]||0) || a.name.localeCompare(b.name)); - for (const v of vulns) { - md.push(`#### ${v.name} β€” ${String(v.severity).toUpperCase()}`); - if (v.range) md.push(`- Vulnerable range: \`${v.range}\``); + // Create table header + md.push(`| Package | Severity | Vulnerable Range | Advisories | Fix Available |`); + md.push(`|---------|----------|------------------|------------|---------------|`); - // "via" can contain strings or advisory objects + for (const v of vulns) { + // Package name + const packageName = `\`${v.name}\``; + + // Severity with emoji + const severityEmoji = { critical: 'πŸ”΄', high: '🟠', moderate: '🟑', low: '🟒', info: 'πŸ”΅' }; + const severity = `${severityEmoji[v.severity] || ''} ${String(v.severity).toUpperCase()}`; + + // Vulnerable range + const range = v.range ? `\`${v.range}\`` : '-'; + + // Advisories const advisories = (v.via || []).filter(x => typeof x === 'object'); const viaStrings = (v.via || []).filter(x => typeof x === 'string'); - - if (viaStrings.length) md.push(`- Via: ${viaStrings.map(s=>`\`${s}\``).join(', ')}`); - - if (advisories.length) { - // Print up to a few advisory lines to keep summary readable - for (const a of advisories.slice(0, 5)) { - md.push(`- ${a.title || 'Advisory'}${a.url ? ` - [View advisory](${a.url})` : ''}`); - if (a.cwe?.length) md.push(` - CWE: ${a.cwe.join(', ')}`); - if (a.cvss?.score != null) md.push(` - CVSS: ${a.cvss.score}`); + let advisoryLinks = []; + if (advisories.length > 0) { + advisoryLinks = advisories.slice(0, 3).map(a => { + const title = (a.title || 'Advisory').substring(0, 50); + return a.url ? `[${title}](${a.url})` : title; + }); + if (advisories.length > 3) { + advisoryLinks.push(`+${advisories.length - 3} more`); } - if (advisories.length > 5) md.push(`- …and ${advisories.length - 5} more advisories`); } - - if (Array.isArray(v.nodes) && v.nodes.length) { - md.push(`- Nodes: ${v.nodes.slice(0,10).map(n=>`\`${n}\``).join(', ')}${v.nodes.length>10 ? ' …' : ''}`); - } - if (Array.isArray(v.effects) && v.effects.length) { - md.push(`- Affects: ${v.effects.map(e=>`\`${e}\``).join(', ')}`); + if (viaStrings.length > 0) { + viaStrings.forEach(s => { + if (!advisoryLinks.some(a => a.includes(s))) { + advisoryLinks.push(`via \`${s}\``); + } + }); } + const advisoryCell = advisoryLinks.length > 0 ? advisoryLinks.join('
') : '-'; + + // Fix available + let fixCell = '❌'; if (v.fixAvailable) { - if (v.fixAvailable === true) md.push(`- Fix available: βœ…`); - else if (v.fixAvailable.isSemVerMajor) { - md.push(`- Fix available: βœ… \`npm audit fix --force\` ⚠️ **Breaking change**`); + if (v.fixAvailable === true) { + fixCell = 'βœ… \`npm audit fix\`'; + } else if (v.fixAvailable.isSemVerMajor) { + fixCell = 'βœ… \`npm audit fix --force\`
⚠️ **Breaking change**'; } else { - md.push(`- Fix available: βœ… \`npm audit fix\``); + fixCell = 'βœ… \`npm audit fix\`'; } - } else { - md.push(`- Fix available: ❌`); } - md.push(''); + + md.push(`| ${packageName} | ${severity} | ${range} | ${advisoryCell} | ${fixCell} |`); } + md.push(''); } // Details (v6) @@ -250,14 +264,21 @@ jobs: const sevRank = { critical:5, high:4, moderate:3, low:2 }; adv.sort((a,b)=> (sevRank[b.severity]||0) - (sevRank[a.severity]||0) || a.module_name.localeCompare(b.module_name)); + // Create table header + md.push(`| Package | Severity | Vulnerable Versions | Patched Versions | Advisory |`); + md.push(`|---------|----------|---------------------|------------------|----------|`); + for (const a of adv) { - md.push(`#### ${a.module_name} β€” ${String(a.severity).toUpperCase()}`); - md.push(`- Title: ${a.title}`); - md.push(`- Vulnerable: \`${a.vulnerable_versions}\``); - md.push(`- Patched: \`${a.patched_versions || 'n/a'}\``); - if (a.url) md.push(`- Advisory: [${a.url}](${a.url})`); - md.push(''); + const packageName = `\`${a.module_name}\``; + const severityEmoji = { critical: 'πŸ”΄', high: '🟠', moderate: '🟑', low: '🟒', info: 'πŸ”΅' }; + const severity = `${severityEmoji[a.severity] || ''} ${String(a.severity).toUpperCase()}`; + const vulnerable = `\`${a.vulnerable_versions}\``; + const patched = a.patched_versions ? `\`${a.patched_versions}\`` : 'n/a'; + const advisory = a.url ? `[View](${a.url})` : a.title || '-'; + + md.push(`| ${packageName} | ${severity} | ${vulnerable} | ${patched} | ${advisory} |`); } + md.push(''); } // Add fix instructions if vulnerabilities found From d3efa63b7c316e251b95c20a85898fb212f93fa9 Mon Sep 17 00:00:00 2001 From: Vucomir Ianculov Date: Wed, 4 Feb 2026 15:10:08 +0200 Subject: [PATCH 09/12] DEVOPS-1274 added shared workflow for npm --- .github/workflows/check-npm-application.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-npm-application.yml b/.github/workflows/check-npm-application.yml index 189bb3e..8071697 100644 --- a/.github/workflows/check-npm-application.yml +++ b/.github/workflows/check-npm-application.yml @@ -119,7 +119,6 @@ jobs: - name: Convert npm audit JSON to Markdown if: ${{ inputs.run_audit && steps.npm-audit-json.outcome != 'skipped' }} id: npm-audit - continue-on-error: true run: | REPORT_FILE=$(mktemp) @@ -316,11 +315,24 @@ jobs: # Cleanup rm -f audit.json - # Exit with appropriate code - if [ "${{ steps.npm-audit-json.outputs.exit_code }}" != "0" ]; then - exit ${{ steps.npm-audit-json.outputs.exit_code }} + - name: Fail on critical or high vulnerabilities + if: ${{ inputs.run_audit && steps.npm-audit.outcome != 'skipped' }} + run: | + CRITICAL="${{ steps.npm-audit.outputs.critical_count || '0' }}" + HIGH="${{ steps.npm-audit.outputs.high_count || '0' }}" + + # Check if critical or high vulnerabilities exist + if [ "$CRITICAL" != "0" ] || [ "$HIGH" != "0" ]; then + echo "❌ Critical or high vulnerabilities found!" + echo "Critical: $CRITICAL" + echo "High: $HIGH" + echo "" + echo "Please review the audit report and fix the vulnerabilities before merging." + exit 1 fi + echo "βœ… No critical or high vulnerabilities found." + - name: Create or update PR comment with audit results if: ${{ inputs.run_audit && github.event_name == 'pull_request' && steps.npm-audit.outcome != 'skipped' }} uses: thollander/actions-comment-pull-request@v3 From 755293e4667d96ed014e6b550031a21e1f842e52 Mon Sep 17 00:00:00 2001 From: Vucomir Ianculov Date: Wed, 4 Feb 2026 15:10:26 +0200 Subject: [PATCH 10/12] DEVOPS-1274 added shared workflow for npm --- .github/workflows/check-npm-application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/check-npm-application.yml b/.github/workflows/check-npm-application.yml index 8071697..895b3a7 100644 --- a/.github/workflows/check-npm-application.yml +++ b/.github/workflows/check-npm-application.yml @@ -317,6 +317,7 @@ jobs: - name: Fail on critical or high vulnerabilities if: ${{ inputs.run_audit && steps.npm-audit.outcome != 'skipped' }} + continue-on-error: true run: | CRITICAL="${{ steps.npm-audit.outputs.critical_count || '0' }}" HIGH="${{ steps.npm-audit.outputs.high_count || '0' }}" From 3af4c4278ce3b1b67552b1e0e014159bc0fcab77 Mon Sep 17 00:00:00 2001 From: Vucomir Ianculov Date: Wed, 4 Feb 2026 15:13:00 +0200 Subject: [PATCH 11/12] DEVOPS-1274 added shared workflow for npm --- .github/workflows/check-npm-application.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/check-npm-application.yml b/.github/workflows/check-npm-application.yml index 895b3a7..8428bd1 100644 --- a/.github/workflows/check-npm-application.yml +++ b/.github/workflows/check-npm-application.yml @@ -315,9 +315,18 @@ jobs: # Cleanup rm -f audit.json + - name: Create or update PR comment with audit results + if: ${{ inputs.run_audit && github.event_name == 'pull_request' && steps.npm-audit.outcome != 'skipped' }} + uses: thollander/actions-comment-pull-request@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + file-path: ${{ steps.npm-audit.outputs.report_file }} + comment-tag: npm-audit-report + mode: upsert + create-if-not-exists: ${{ steps.npm-audit.outputs.has_vulnerabilities || 'false' }} + - name: Fail on critical or high vulnerabilities if: ${{ inputs.run_audit && steps.npm-audit.outcome != 'skipped' }} - continue-on-error: true run: | CRITICAL="${{ steps.npm-audit.outputs.critical_count || '0' }}" HIGH="${{ steps.npm-audit.outputs.high_count || '0' }}" @@ -333,13 +342,3 @@ jobs: fi echo "βœ… No critical or high vulnerabilities found." - - - name: Create or update PR comment with audit results - if: ${{ inputs.run_audit && github.event_name == 'pull_request' && steps.npm-audit.outcome != 'skipped' }} - uses: thollander/actions-comment-pull-request@v3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - file-path: ${{ steps.npm-audit.outputs.report_file }} - comment-tag: npm-audit-report - mode: upsert - create-if-not-exists: ${{ steps.npm-audit.outputs.has_vulnerabilities || 'false' }} From 1c88c9afcb3e6de3a244b9c3db61db0738af1df7 Mon Sep 17 00:00:00 2001 From: Vucomir Ianculov Date: Wed, 4 Feb 2026 15:15:07 +0200 Subject: [PATCH 12/12] DEVOPS-1274 added shared workflow for npm --- .github/workflows/check-npm-application.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-npm-application.yml b/.github/workflows/check-npm-application.yml index 8428bd1..22bd420 100644 --- a/.github/workflows/check-npm-application.yml +++ b/.github/workflows/check-npm-application.yml @@ -65,6 +65,11 @@ on: type: string description: "The value of --audit-level flag" default: "moderate" + fail_on_critical_high: + required: false + type: boolean + description: "Whether to fail the workflow on critical or high vulnerabilities" + default: true working_directory: required: false type: string @@ -326,7 +331,7 @@ jobs: create-if-not-exists: ${{ steps.npm-audit.outputs.has_vulnerabilities || 'false' }} - name: Fail on critical or high vulnerabilities - if: ${{ inputs.run_audit && steps.npm-audit.outcome != 'skipped' }} + if: ${{ inputs.run_audit && inputs.fail_on_critical_high && steps.npm-audit.outcome != 'skipped' }} run: | CRITICAL="${{ steps.npm-audit.outputs.critical_count || '0' }}" HIGH="${{ steps.npm-audit.outputs.high_count || '0' }}"