@@ -72,6 +72,9 @@ concurrency:
7272env :
7373 INPUT_FAIL_ON_ERROR : ${{ inputs.linkcheck_fail_on_error || 'true' }}
7474 INPUT_ISSUE_ON_ERROR : ${{ inputs.linkcheck_create_issue || 'false' }}
75+ # Effective ref for link checker remap: prefer inputs.ref if it looks like a branch name, else fall back to base_ref/ref_name
76+ # Heuristic: use inputs.ref if provided, doesn't contain '/', and doesn't start with 'v' (likely a tag)
77+ EFFECTIVE_REF : ${{ (inputs.ref && !contains(inputs.ref, '/') && !startsWith(inputs.ref, 'v')) && inputs.ref || github.base_ref || github.ref_name }}
7578 MAVEN_VERSION : 3.9.8
7679 JAVA_DISTRO : ' temurin'
7780 JAVA_VERSION_FILE : .java-version
@@ -166,6 +169,25 @@ jobs:
166169 echo "**Language:** $FILENAME" >> $GITHUB_STEP_SUMMARY
167170 echo "- Results found: $RESULTS" >> $GITHUB_STEP_SUMMARY
168171 echo "- Rules checked: $RULES" >> $GITHUB_STEP_SUMMARY
172+ # Show details if there are findings
173+ if [ "$RESULTS" -gt 0 ]; then
174+ echo "" >> $GITHUB_STEP_SUMMARY
175+ echo "<details>" >> $GITHUB_STEP_SUMMARY
176+ echo "<summary>View $RESULTS finding(s)</summary>" >> $GITHUB_STEP_SUMMARY
177+ echo "" >> $GITHUB_STEP_SUMMARY
178+ echo "| Severity | Sec-Sev | Rule | Location | Message |" >> $GITHUB_STEP_SUMMARY
179+ echo "|----------|---------|------|----------|---------|" >> $GITHUB_STEP_SUMMARY
180+ # Join results with rules to get security-severity (which is on rule definitions, not results)
181+ jq -r '
182+ (.runs[0].tool.driver.rules // []) as $driver_rules |
183+ ([.runs[0].tool.extensions[]?.rules // []] | add // []) as $ext_rules |
184+ ($driver_rules + $ext_rules | map({(.id): (.properties["security-severity"] // null)}) | add // {}) as $severities |
185+ .runs[0].results[] |
186+ "| \(if .level == "error" then "Error" elif .level == "warning" then "Warning" elif .level == "note" then "Note" else .level end) | \($severities[.ruleId] // "N/A") | \(.ruleId // "unknown") | `\(.locations[0].physicalLocation.artifactLocation.uri // "unknown"):\(.locations[0].physicalLocation.region.startLine // "?")` | \(.message.text | gsub("\n"; " ") | gsub("\\|"; "\\\\|") | .[0:120]) |"
187+ ' "$sarif" >> $GITHUB_STEP_SUMMARY
188+ echo "" >> $GITHUB_STEP_SUMMARY
189+ echo "</details>" >> $GITHUB_STEP_SUMMARY
190+ fi
169191 fi
170192 done
171193 else
@@ -199,16 +221,28 @@ jobs:
199221 echo "" >> $GITHUB_STEP_SUMMARY
200222 if [ -f "trivy-results.sarif" ]; then
201223 TOTAL=$(jq -r '.runs[0].results | length' trivy-results.sarif 2>/dev/null || echo "0")
202- # Trivy SARIF level mapping: error= CRITICAL, warning=HIGH, note=MEDIUM/ LOW
203- CRITICAL =$(jq -r '[.runs[0].results[] | select(.level == "error")] | length' trivy-results.sarif 2>/dev/null || echo "0")
204- HIGH =$(jq -r '[.runs[0].results[] | select(.level == "warning")] | length' trivy-results.sarif 2>/dev/null || echo "0")
205- MEDIUM_LOW =$(jq -r '[.runs[0].results[] | select(.level == "note")] | length' trivy-results.sarif 2>/dev/null || echo "0")
224+ # Trivy SARIF level mapping (from trivy source): CRITICAL+HIGH -> error, MEDIUM -> warning, LOW+UNKNOWN -> note
225+ CRITICAL_HIGH =$(jq -r '[.runs[0].results[] | select(.level == "error")] | length' trivy-results.sarif 2>/dev/null || echo "0")
226+ MEDIUM =$(jq -r '[.runs[0].results[] | select(.level == "warning")] | length' trivy-results.sarif 2>/dev/null || echo "0")
227+ LOW =$(jq -r '[.runs[0].results[] | select(.level == "note")] | length' trivy-results.sarif 2>/dev/null || echo "0")
206228 echo "| Severity | Count |" >> $GITHUB_STEP_SUMMARY
207229 echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
208- echo "| :red_circle: Critical | $CRITICAL |" >> $GITHUB_STEP_SUMMARY
209- echo "| :orange_circle: High | $HIGH |" >> $GITHUB_STEP_SUMMARY
210- echo "| :yellow_circle: Medium/ Low | $MEDIUM_LOW |" >> $GITHUB_STEP_SUMMARY
230+ echo "| :red_circle: Critical/High | $CRITICAL_HIGH |" >> $GITHUB_STEP_SUMMARY
231+ echo "| :orange_circle: Medium | $MEDIUM |" >> $GITHUB_STEP_SUMMARY
232+ echo "| :yellow_circle: Low | $LOW |" >> $GITHUB_STEP_SUMMARY
211233 echo "| **Total** | **$TOTAL** |" >> $GITHUB_STEP_SUMMARY
234+ # Show details if there are findings
235+ if [ "$TOTAL" -gt 0 ]; then
236+ echo "" >> $GITHUB_STEP_SUMMARY
237+ echo "<details>" >> $GITHUB_STEP_SUMMARY
238+ echo "<summary>View $TOTAL finding(s)</summary>" >> $GITHUB_STEP_SUMMARY
239+ echo "" >> $GITHUB_STEP_SUMMARY
240+ echo "| Severity | Rule | Location | Message |" >> $GITHUB_STEP_SUMMARY
241+ echo "|----------|------|----------|---------|" >> $GITHUB_STEP_SUMMARY
242+ jq -r '.runs[0].results[] | "| \(if .level == "error" then "Critical/High" elif .level == "warning" then "Medium" elif .level == "note" then "Low" else .level end) | \(.ruleId // "unknown") | `\(.locations[0].physicalLocation.artifactLocation.uri // "unknown"):\(.locations[0].physicalLocation.region.startLine // "?")` | \(.message.text | gsub("\n"; " ") | gsub("\\|"; "\\\\|") | .[0:120]) |"' trivy-results.sarif >> $GITHUB_STEP_SUMMARY
243+ echo "" >> $GITHUB_STEP_SUMMARY
244+ echo "</details>" >> $GITHUB_STEP_SUMMARY
245+ fi
212246 else
213247 echo "No Trivy results file found." >> $GITHUB_STEP_SUMMARY
214248 fi
@@ -222,14 +256,58 @@ jobs:
222256 if : ${{ !inputs.skip_code_scans && env.UPLOAD_SCAN_SARIF == 'true' }}
223257 uses : github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7
224258 with :
225- sarif_file : codeql-results/java.sarif
259+ sarif_file : codeql-results
226260 category : ' codeql'
227261 - name : Upload Trivy scan results to GitHub Security tab
228262 if : ${{ !inputs.skip_code_scans && env.UPLOAD_SCAN_SARIF == 'true' }}
229263 uses : github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7
230264 with :
231265 sarif_file : ' trivy-results.sarif'
232266 category : ' trivy'
267+ - name : Fail on critical/high security findings
268+ if : ${{ !inputs.skip_code_scans }}
269+ run : |
270+ FAILED=false
271+ # Check CodeQL for error level (critical/high) findings
272+ if [ -d "codeql-results" ]; then
273+ for sarif in codeql-results/*.sarif; do
274+ if [ -f "$sarif" ]; then
275+ # Check for error level OR security-severity >= 7.0 (high/critical)
276+ # Note: security-severity is on rule definitions, not results, so we join via ruleId
277+ CODEQL_CRITICAL=$(jq -r '
278+ # Collect security-severity from driver and extension rules
279+ (.runs[0].tool.driver.rules // []) as $driver_rules |
280+ ([.runs[0].tool.extensions[]?.rules // []] | add // []) as $ext_rules |
281+ ($driver_rules + $ext_rules | map({(.id): (.properties["security-severity"] // "0")}) | add // {}) as $severities |
282+ [.runs[0].results[] | select(
283+ .level == "error" or
284+ (($severities[.ruleId] // "0") | tonumber >= 7.0)
285+ )] | length
286+ ' "$sarif" 2>/dev/null || echo "0")
287+ if [ "$CODEQL_CRITICAL" -gt 0 ]; then
288+ echo "::error::CodeQL found $CODEQL_CRITICAL critical/high severity issue(s)"
289+ FAILED=true
290+ fi
291+ fi
292+ done
293+ fi
294+ # Check Trivy for critical/high (error) and medium (warning) findings
295+ # Note: Trivy SARIF mapping: CRITICAL+HIGH -> error, MEDIUM -> warning, LOW -> note
296+ if [ -f "trivy-results.sarif" ]; then
297+ TRIVY_CRITICAL_HIGH=$(jq -r '[.runs[0].results[] | select(.level == "error")] | length' trivy-results.sarif 2>/dev/null || echo "0")
298+ TRIVY_MEDIUM=$(jq -r '[.runs[0].results[] | select(.level == "warning")] | length' trivy-results.sarif 2>/dev/null || echo "0")
299+ if [ "$TRIVY_CRITICAL_HIGH" -gt 0 ]; then
300+ echo "::error::Trivy found $TRIVY_CRITICAL_HIGH critical/high severity issue(s)"
301+ FAILED=true
302+ fi
303+ if [ "$TRIVY_MEDIUM" -gt 0 ]; then
304+ echo "::warning::Trivy found $TRIVY_MEDIUM medium severity issue(s)"
305+ fi
306+ fi
307+ if [ "$FAILED" = true ]; then
308+ echo "::error::Build failed due to critical/high security findings. See summaries above for details."
309+ exit 1
310+ fi
233311 build-website :
234312 name : Website
235313 runs-on : ubuntu-24.04
@@ -276,7 +354,7 @@ jobs:
276354 if : ${{ !inputs.skip_linkcheck }}
277355 uses : lycheeverse/lychee-action@a8c4c7cb88f0c7386610c35eb25108e448569cb0
278356 with :
279- args : --verbose --no-progress --accept 200,206,429,503 './target/staging/**/*.html' --remap "https://github.com/metaschema-framework/liboscal-java/tree/main / file://${GITHUB_WORKSPACE}/" --remap "https://liboscal-java.metaschema.dev/ file://${GITHUB_WORKSPACE}/target/staging/"
357+ args : --verbose --no-progress --accept 200,206,429,503 './target/staging/**/*.html' --remap "https://github.com/metaschema-framework/liboscal-java/tree/${{ env.EFFECTIVE_REF }} / file://${GITHUB_WORKSPACE}/" --remap "https://liboscal-java.metaschema.dev/ file://${GITHUB_WORKSPACE}/target/staging/"
280358 format : markdown
281359 output : html-link-report.md
282360 debug : true
@@ -287,19 +365,25 @@ jobs:
287365 - name : Link Checker Summary
288366 if : ${{ !inputs.skip_linkcheck && always() }}
289367 run : |
290- echo "<details>" >> $GITHUB_STEP_SUMMARY
291- echo "<summary><h2>Link Checker Results</h2></summary>" >> $GITHUB_STEP_SUMMARY
368+ echo "## Link Checker Results" >> $GITHUB_STEP_SUMMARY
292369 echo "" >> $GITHUB_STEP_SUMMARY
293370 if [ -f "html-link-report.md" ]; then
294- # Extract summary stats from the report (lychee markdown format uses "- [Status]")
295- ERRORS=$(grep -c "^- \[Broken\]" html-link-report.md 2>/dev/null || echo "0")
296- TIMEOUTS=$(grep -c "^- \[Timeout\]" html-link-report.md 2>/dev/null || echo "0")
371+ # Extract summary stats from the report
372+ # Lychee uses [ERROR], [4xx], [5xx] for broken links and [TIMEOUT] for timeouts
373+ # Note: grep -c exits 1 when no matches, so we capture output first then handle exit code
374+ ERRORS=$(grep -cE "^\[ERROR\]|^\[[45][0-9]{2}\]" html-link-report.md 2>/dev/null) || ERRORS=0
375+ TIMEOUTS=$(grep -c "^\[TIMEOUT\]" html-link-report.md 2>/dev/null) || TIMEOUTS=0
297376 if [ "$ERRORS" -gt 0 ]; then
298377 echo ":x: **Found $ERRORS broken link(s)**" >> $GITHUB_STEP_SUMMARY
299378 echo "" >> $GITHUB_STEP_SUMMARY
379+ echo "<details>" >> $GITHUB_STEP_SUMMARY
380+ echo "<summary>View broken links</summary>" >> $GITHUB_STEP_SUMMARY
381+ echo "" >> $GITHUB_STEP_SUMMARY
300382 echo '```' >> $GITHUB_STEP_SUMMARY
301- grep "^- \[Broken \]" html-link-report.md >> $GITHUB_STEP_SUMMARY
383+ grep -E "^\[ERROR\]|^\[[45][0-9]{2} \]" html-link-report.md >> $GITHUB_STEP_SUMMARY
302384 echo '```' >> $GITHUB_STEP_SUMMARY
385+ echo "" >> $GITHUB_STEP_SUMMARY
386+ echo "</details>" >> $GITHUB_STEP_SUMMARY
303387 elif [ "$TIMEOUTS" -gt 0 ]; then
304388 echo ":warning: **$TIMEOUTS link(s) timed out** (external sites may be slow)" >> $GITHUB_STEP_SUMMARY
305389 else
@@ -309,7 +393,6 @@ jobs:
309393 echo ":warning: No link check report found." >> $GITHUB_STEP_SUMMARY
310394 fi
311395 echo "" >> $GITHUB_STEP_SUMMARY
312- echo "</details>" >> $GITHUB_STEP_SUMMARY
313396 - name : Upload link check report
314397 if : ${{ !inputs.skip_linkcheck }}
315398 uses : actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
@@ -319,7 +402,7 @@ jobs:
319402 retention-days : 5
320403 - name : Create issue if bad links detected
321404 # Only create issues for actual broken links (exit code 2), not timeouts (exit code 1)
322- if : ${{ !inputs.skip_linkcheck && !cancelled() && steps.linkchecker.outputs.exit_code == 2 && env.INPUT_ISSUE_ON_ERROR == 'true' }}
405+ if : ${{ !inputs.skip_linkcheck && !cancelled() && steps.linkchecker.outputs.exit_code == '2' && env.INPUT_ISSUE_ON_ERROR == 'true' }}
323406 uses : peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710
324407 with :
325408 title : Scheduled Check of Website Content Found Bad Hyperlinks
@@ -328,10 +411,13 @@ jobs:
328411 bug
329412 documentation
330413 - name : Fail on link check error
331- # Exit codes: 0=success, 1=runtime errors/timeouts, 2=broken links found
332- # Only fail on actual broken links (exit code 2), not timeouts (exit code 1)
333- if : ${{ !inputs.skip_linkcheck && !cancelled() && steps.linkchecker.outputs.exit_code == 2 && env.INPUT_FAIL_ON_ERROR == 'true' }}
334- uses : actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
335- with :
336- script : |
337- core.setFailed('Link checker detected broken or invalid links, read attached report.')
414+ # Check report for actual broken links (ERROR, 4xx, 5xx), not timeouts
415+ if : ${{ !inputs.skip_linkcheck && !cancelled() && env.INPUT_FAIL_ON_ERROR == 'true' }}
416+ run : |
417+ if [ -f "html-link-report.md" ]; then
418+ ERRORS=$(grep -cE "^\[ERROR\]|^\[[45][0-9]{2}\]" html-link-report.md 2>/dev/null) || ERRORS=0
419+ if [ "$ERRORS" -gt 0 ]; then
420+ echo "::error::Link checker found $ERRORS broken link(s). See report for details."
421+ exit 1
422+ fi
423+ fi
0 commit comments