diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index fdb40db5..a08ade8d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -72,6 +72,8 @@ concurrency:
env:
INPUT_FAIL_ON_ERROR: ${{ inputs.linkcheck_fail_on_error || 'true' }}
INPUT_ISSUE_ON_ERROR: ${{ inputs.linkcheck_create_issue || 'false' }}
+ # Effective ref for link checker remap: use inputs.ref if provided, else base_ref (PRs) or ref_name (pushes), fallback to develop
+ EFFECTIVE_REF: ${{ inputs.ref || github.base_ref || github.ref_name || 'develop' }}
MAVEN_VERSION: 3.9.8
JAVA_DISTRO: 'temurin'
JAVA_VERSION_FILE: .java-version
@@ -166,6 +168,25 @@ jobs:
echo "**Language:** $FILENAME" >> $GITHUB_STEP_SUMMARY
echo "- Results found: $RESULTS" >> $GITHUB_STEP_SUMMARY
echo "- Rules checked: $RULES" >> $GITHUB_STEP_SUMMARY
+ # Show details if there are findings
+ if [ "$RESULTS" -gt 0 ]; then
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "View $RESULTS finding(s)
" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Severity | Sec-Sev | Rule | Location | Message |" >> $GITHUB_STEP_SUMMARY
+ echo "|----------|---------|------|----------|---------|" >> $GITHUB_STEP_SUMMARY
+ # Join results with rules to get security-severity (which is on rule definitions, not results)
+ jq -r '
+ (.runs[0].tool.driver.rules // []) as $driver_rules |
+ ([.runs[0].tool.extensions[]?.rules // []] | add // []) as $ext_rules |
+ ($driver_rules + $ext_rules | map({(.id): (.properties["security-severity"] // null)}) | add // {}) as $severities |
+ .runs[0].results[] |
+ "| \(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]) |"
+ ' "$sarif" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo " " >> $GITHUB_STEP_SUMMARY
+ fi
fi
done
else
@@ -199,16 +220,28 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY
if [ -f "trivy-results.sarif" ]; then
TOTAL=$(jq -r '.runs[0].results | length' trivy-results.sarif 2>/dev/null || echo "0")
- # Trivy SARIF level mapping: error=CRITICAL, warning=HIGH, note=MEDIUM/LOW
- CRITICAL=$(jq -r '[.runs[0].results[] | select(.level == "error")] | length' trivy-results.sarif 2>/dev/null || echo "0")
- HIGH=$(jq -r '[.runs[0].results[] | select(.level == "warning")] | length' trivy-results.sarif 2>/dev/null || echo "0")
- MEDIUM_LOW=$(jq -r '[.runs[0].results[] | select(.level == "note")] | length' trivy-results.sarif 2>/dev/null || echo "0")
+ # Trivy SARIF level mapping (from trivy source): CRITICAL+HIGH -> error, MEDIUM -> warning, LOW+UNKNOWN -> note
+ CRITICAL_HIGH=$(jq -r '[.runs[0].results[] | select(.level == "error")] | length' trivy-results.sarif 2>/dev/null || echo "0")
+ MEDIUM=$(jq -r '[.runs[0].results[] | select(.level == "warning")] | length' trivy-results.sarif 2>/dev/null || echo "0")
+ LOW=$(jq -r '[.runs[0].results[] | select(.level == "note")] | length' trivy-results.sarif 2>/dev/null || echo "0")
echo "| Severity | Count |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
- echo "| :red_circle: Critical | $CRITICAL |" >> $GITHUB_STEP_SUMMARY
- echo "| :orange_circle: High | $HIGH |" >> $GITHUB_STEP_SUMMARY
- echo "| :yellow_circle: Medium/Low | $MEDIUM_LOW |" >> $GITHUB_STEP_SUMMARY
+ echo "| :red_circle: Critical/High | $CRITICAL_HIGH |" >> $GITHUB_STEP_SUMMARY
+ echo "| :orange_circle: Medium | $MEDIUM |" >> $GITHUB_STEP_SUMMARY
+ echo "| :yellow_circle: Low | $LOW |" >> $GITHUB_STEP_SUMMARY
echo "| **Total** | **$TOTAL** |" >> $GITHUB_STEP_SUMMARY
+ # Show details if there are findings
+ if [ "$TOTAL" -gt 0 ]; then
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "View $TOTAL finding(s)
" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Severity | Rule | Location | Message |" >> $GITHUB_STEP_SUMMARY
+ echo "|----------|------|----------|---------|" >> $GITHUB_STEP_SUMMARY
+ 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
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo " " >> $GITHUB_STEP_SUMMARY
+ fi
else
echo "No Trivy results file found." >> $GITHUB_STEP_SUMMARY
fi
@@ -222,7 +255,7 @@ jobs:
if: ${{ !inputs.skip_code_scans && env.UPLOAD_SCAN_SARIF == 'true' }}
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7
with:
- sarif_file: codeql-results/java.sarif
+ sarif_file: codeql-results
category: 'codeql'
- name: Upload Trivy scan results to GitHub Security tab
if: ${{ !inputs.skip_code_scans && env.UPLOAD_SCAN_SARIF == 'true' }}
@@ -230,6 +263,50 @@ jobs:
with:
sarif_file: 'trivy-results.sarif'
category: 'trivy'
+ - name: Fail on critical/high security findings
+ if: ${{ !inputs.skip_code_scans }}
+ run: |
+ FAILED=false
+ # Check CodeQL for error level (critical/high) findings
+ if [ -d "codeql-results" ]; then
+ for sarif in codeql-results/*.sarif; do
+ if [ -f "$sarif" ]; then
+ # Check for error level OR security-severity >= 7.0 (high/critical)
+ # Note: security-severity is on rule definitions, not results, so we join via ruleId
+ CODEQL_CRITICAL=$(jq -r '
+ # Collect security-severity from driver and extension rules
+ (.runs[0].tool.driver.rules // []) as $driver_rules |
+ ([.runs[0].tool.extensions[]?.rules // []] | add // []) as $ext_rules |
+ ($driver_rules + $ext_rules | map({(.id): (.properties["security-severity"] // "0")}) | add // {}) as $severities |
+ [.runs[0].results[] | select(
+ .level == "error" or
+ (($severities[.ruleId] // "0") | tonumber >= 7.0)
+ )] | length
+ ' "$sarif" 2>/dev/null || echo "0")
+ if [ "$CODEQL_CRITICAL" -gt 0 ]; then
+ echo "::error::CodeQL found $CODEQL_CRITICAL critical/high severity issue(s)"
+ FAILED=true
+ fi
+ fi
+ done
+ fi
+ # Check Trivy for critical/high (error) and medium (warning) findings
+ # Note: Trivy SARIF mapping: CRITICAL+HIGH -> error, MEDIUM -> warning, LOW -> note
+ if [ -f "trivy-results.sarif" ]; then
+ TRIVY_CRITICAL_HIGH=$(jq -r '[.runs[0].results[] | select(.level == "error")] | length' trivy-results.sarif 2>/dev/null || echo "0")
+ TRIVY_MEDIUM=$(jq -r '[.runs[0].results[] | select(.level == "warning")] | length' trivy-results.sarif 2>/dev/null || echo "0")
+ if [ "$TRIVY_CRITICAL_HIGH" -gt 0 ]; then
+ echo "::error::Trivy found $TRIVY_CRITICAL_HIGH critical/high severity issue(s)"
+ FAILED=true
+ fi
+ if [ "$TRIVY_MEDIUM" -gt 0 ]; then
+ echo "::warning::Trivy found $TRIVY_MEDIUM medium severity issue(s)"
+ fi
+ fi
+ if [ "$FAILED" = true ]; then
+ echo "::error::Build failed due to critical/high security findings. See summaries above for details."
+ exit 1
+ fi
build-website:
name: Website
runs-on: ubuntu-24.04
@@ -276,7 +353,7 @@ jobs:
if: ${{ !inputs.skip_linkcheck }}
uses: lycheeverse/lychee-action@a8c4c7cb88f0c7386610c35eb25108e448569cb0
with:
- 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/"
+ 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/"
format: markdown
output: html-link-report.md
debug: true
@@ -287,19 +364,25 @@ jobs:
- name: Link Checker Summary
if: ${{ !inputs.skip_linkcheck && always() }}
run: |
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "Link Checker Results
" >> $GITHUB_STEP_SUMMARY
+ echo "## Link Checker Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -f "html-link-report.md" ]; then
- # Extract summary stats from the report (lychee markdown format uses "- [Status]")
- ERRORS=$(grep -c "^- \[Broken\]" html-link-report.md 2>/dev/null || echo "0")
- TIMEOUTS=$(grep -c "^- \[Timeout\]" html-link-report.md 2>/dev/null || echo "0")
+ # Extract summary stats from the report
+ # Lychee uses [ERROR], [4xx], [5xx] for broken links and [TIMEOUT] for timeouts
+ # Note: grep -c exits 1 when no matches, so we capture output first then handle exit code
+ ERRORS=$(grep -cE "^\[ERROR\]|^\[[45][0-9]{2}\]" html-link-report.md 2>/dev/null) || ERRORS=0
+ TIMEOUTS=$(grep -c "^\[TIMEOUT\]" html-link-report.md 2>/dev/null) || TIMEOUTS=0
if [ "$ERRORS" -gt 0 ]; then
echo ":x: **Found $ERRORS broken link(s)**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "View broken links
" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
- grep "^- \[Broken\]" html-link-report.md >> $GITHUB_STEP_SUMMARY
+ grep -E "^\[ERROR\]|^\[[45][0-9]{2}\]" html-link-report.md >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo " " >> $GITHUB_STEP_SUMMARY
elif [ "$TIMEOUTS" -gt 0 ]; then
echo ":warning: **$TIMEOUTS link(s) timed out** (external sites may be slow)" >> $GITHUB_STEP_SUMMARY
else
@@ -309,7 +392,6 @@ jobs:
echo ":warning: No link check report found." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
- echo " " >> $GITHUB_STEP_SUMMARY
- name: Upload link check report
if: ${{ !inputs.skip_linkcheck }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
@@ -319,7 +401,7 @@ jobs:
retention-days: 5
- name: Create issue if bad links detected
# Only create issues for actual broken links (exit code 2), not timeouts (exit code 1)
- if: ${{ !inputs.skip_linkcheck && !cancelled() && steps.linkchecker.outputs.exit_code == 2 && env.INPUT_ISSUE_ON_ERROR == 'true' }}
+ if: ${{ !inputs.skip_linkcheck && !cancelled() && steps.linkchecker.outputs.exit_code == '2' && env.INPUT_ISSUE_ON_ERROR == 'true' }}
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710
with:
title: Scheduled Check of Website Content Found Bad Hyperlinks
@@ -328,10 +410,13 @@ jobs:
bug
documentation
- name: Fail on link check error
- # Exit codes: 0=success, 1=runtime errors/timeouts, 2=broken links found
- # Only fail on actual broken links (exit code 2), not timeouts (exit code 1)
- if: ${{ !inputs.skip_linkcheck && !cancelled() && steps.linkchecker.outputs.exit_code == 2 && env.INPUT_FAIL_ON_ERROR == 'true' }}
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
- with:
- script: |
- core.setFailed('Link checker detected broken or invalid links, read attached report.')
+ # Check report for actual broken links (ERROR, 4xx, 5xx), not timeouts
+ if: ${{ !inputs.skip_linkcheck && !cancelled() && env.INPUT_FAIL_ON_ERROR == 'true' }}
+ run: |
+ if [ -f "html-link-report.md" ]; then
+ ERRORS=$(grep -cE "^\[ERROR\]|^\[[45][0-9]{2}\]" html-link-report.md 2>/dev/null) || ERRORS=0
+ if [ "$ERRORS" -gt 0 ]; then
+ echo "::error::Link checker found $ERRORS broken link(s). See report for details."
+ exit 1
+ fi
+ fi