-
Notifications
You must be signed in to change notification settings - Fork 5
422 lines (422 loc) · 19.9 KB
/
Copy pathbuild.yml
File metadata and controls
422 lines (422 loc) · 19.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
on:
push:
branches:
- main
- release/**
- develop
- feature/**
pull_request:
types: [opened, synchronize, reopened]
branches:
- main
- release/**
- develop
- feature/**
merge_group:
workflow_call:
inputs:
ref:
description: 'Git ref to checkout (branch, tag, or SHA)'
required: false
type: string
skip_code_scans:
description: 'Skip CodeQL and Trivy security scans'
required: false
default: false
type: boolean
skip_linkcheck:
description: 'Skip website link checker'
required: false
default: false
type: boolean
linkcheck_fail_on_error:
description: 'a boolean flag that determines if bad links found by the link checker fail fast and stop a complete build'
required: false
default: true
type: boolean
linkcheck_create_issue:
description: 'create new GitHub issue if broken links found'
required: false
default: false
type: boolean
workflow_dispatch:
inputs:
ref:
description: 'Git ref to checkout (branch, tag, or SHA)'
required: false
type: string
skip_code_scans:
description: 'Skip CodeQL and Trivy security scans'
required: false
default: false
type: boolean
skip_linkcheck:
description: 'Skip website link checker'
required: false
default: false
type: boolean
linkcheck_fail_on_error:
description: 'a boolean flag that determines if bad links found by the link checker fail fast and stop a complete build'
required: false
default: true
type: boolean
linkcheck_create_issue:
description: 'create new GitHub issue if broken links found'
required: false
default: false
type: boolean
name: Build and Test
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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
# Post Maven artifacts to the artifact repo if the branch is 'develop' or 'release/*'. This avoids publishing artifacts for pull requests
COMMIT_MAVEN_ARTIFACTS: ${{ (github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/release/')) && github.repository_owner == 'metaschema-framework' }}
# Upload security scan SARIF results for main, develop, release/* branches, or PRs targeting these branches.
UPLOAD_SCAN_SARIF: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/release/')) || (github.event_name == 'pull_request' && (github.base_ref == 'main' || github.base_ref == 'develop' || startsWith(github.base_ref, 'release/'))) }}
jobs:
build-code:
name: Code
runs-on: ubuntu-24.04
permissions:
actions: read
contents: read
security-events: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
ref: ${{ inputs.ref || github.ref }}
submodules: recursive
filter: tree:0
- name: Checkout maven2 branch
if: env.COMMIT_MAVEN_ARTIFACTS == 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
path: maven2
ref: main
repository: metaschema-framework/maven2
token: ${{ secrets.ACCESS_TOKEN }}
fetch-depth: 2
persist-credentials: true
# -------------------------
# Java Environment Setup
# -------------------------
- name: Set up Maven
uses: stCarolas/setup-maven@d6af6abeda15e98926a57b5aa970a96bb37f97d1
with:
maven-version: ${{ env.MAVEN_VERSION }}
- name: Set up JDK
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654
with:
java-version-file: ${{ env.JAVA_VERSION_FILE }}
distribution: ${{ env.JAVA_DISTRO }}
cache: 'maven'
- name: Initialize CodeQL
if: ${{ !inputs.skip_code_scans }}
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7
with:
languages: java
config-file: .github/codeql-config.yml
# -------------------------
# Maven Build
# -------------------------
- name: Build and Test Code
run: |
mvn -B -e -Prelease -Psnapshots -DaltDeploymentRepository=repo-snapshot::file://${GITHUB_WORKSPACE}/maven2/ -DaltSnapshotDeploymentRepository=repo-snapshot::file://${GITHUB_WORKSPACE}/maven2/ -DrepositoryId=repo-snapshot deploy
- name: Deploy Artifacts
if: env.COMMIT_MAVEN_ARTIFACTS == 'true'
run: |
MVN_COORDS=$(echo '${project.groupId}:${project.artifactId}:${project.version}' | mvn -N -q -DforceStdout help:evaluate)
cd maven2
echo "Configuring git identity"
git config user.name "GitHub Action"
git config user.email "action@github.com"
echo "Comitting artifacts"
git add -A
git commit -m "[CI SKIP] Deploying artifacts for $MVN_COORDS."
echo "Syncing with latest"
git pull -r -s ours
echo "Pushing changes"
git push --force-with-lease
- name: Perform CodeQL Analysis
if: ${{ !inputs.skip_code_scans }}
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7
with:
upload: 'never'
output: codeql-results
- name: CodeQL Summary
if: ${{ !inputs.skip_code_scans }}
run: |
echo "## CodeQL Security Scan Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -d "codeql-results" ]; then
for sarif in codeql-results/*.sarif; do
if [ -f "$sarif" ]; then
FILENAME=$(basename "$sarif" .sarif)
RESULTS=$(jq -r '.runs[0].results | length' "$sarif" 2>/dev/null || echo "0")
# Count rules from driver and all extensions
DRIVER_RULES=$(jq -r '.runs[0].tool.driver.rules // [] | length' "$sarif" 2>/dev/null || echo "0")
EXT_RULES=$(jq -r '[.runs[0].tool.extensions[]?.rules // [] | length] | add // 0' "$sarif" 2>/dev/null || echo "0")
RULES=$((DRIVER_RULES + EXT_RULES))
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 "<details>" >> $GITHUB_STEP_SUMMARY
echo "<summary>View $RESULTS finding(s)</summary>" >> $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 "</details>" >> $GITHUB_STEP_SUMMARY
fi
fi
done
else
echo "No CodeQL results directory found." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ env.UPLOAD_SCAN_SARIF }}" == "true" ]; then
echo ":white_check_mark: Results uploaded to GitHub Security tab" >> $GITHUB_STEP_SUMMARY
else
echo ":information_source: Results not uploaded (branch/PR not targeting main, develop, or release)" >> $GITHUB_STEP_SUMMARY
fi
# -------------------------
# Trivy Security Scan
# -------------------------
- name: Run Trivy security scanner
if: ${{ !inputs.skip_code_scans }}
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: 'fs'
scan-ref: '.'
scanners: 'vuln,secret,misconfig'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN'
# Exclude submodule and maven2 artifact repo checkout (have their own security scanning)
skip-dirs: 'oscal,maven2'
- name: Trivy Summary
if: ${{ !inputs.skip_code_scans }}
run: |
echo "## Trivy Security Scan Results" >> $GITHUB_STEP_SUMMARY
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 (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/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 "<details>" >> $GITHUB_STEP_SUMMARY
echo "<summary>View $TOTAL finding(s)</summary>" >> $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 "</details>" >> $GITHUB_STEP_SUMMARY
fi
else
echo "No Trivy results file found." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ env.UPLOAD_SCAN_SARIF }}" == "true" ]; then
echo ":white_check_mark: Results uploaded to GitHub Security tab" >> $GITHUB_STEP_SUMMARY
else
echo ":information_source: Results not uploaded (branch/PR not targeting main, develop, or release)" >> $GITHUB_STEP_SUMMARY
fi
- name: Upload CodeQL scan results to GitHub Security tab
if: ${{ !inputs.skip_code_scans && env.UPLOAD_SCAN_SARIF == 'true' }}
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7
with:
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' }}
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7
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
permissions:
actions: read
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
ref: ${{ inputs.ref || github.ref }}
submodules: recursive
filter: tree:0
# -------------------------
# Java Environment Setup
# -------------------------
- name: Set up Maven
uses: stCarolas/setup-maven@d6af6abeda15e98926a57b5aa970a96bb37f97d1
with:
maven-version: ${{ env.MAVEN_VERSION }}
- name: Set up JDK
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654
with:
java-version-file: ${{ env.JAVA_VERSION_FILE }}
distribution: ${{ env.JAVA_DISTRO }}
cache: 'maven'
# -------------------------
# Maven Build
# -------------------------
- name: Build and Test Website
run: |
mvn -B -e -PCI -Prelease install site site:stage -Dmaven.test.skip=true
- name: Zip Artifacts for Upload
run: |
zip ${{ runner.temp }}/website.zip -r target/staging
- name: Upload generated site
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: website
path: |
${{ runner.temp }}/website.zip
retention-days: 5
- id: linkchecker
name: Link Checker
if: ${{ !inputs.skip_linkcheck }}
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411
with:
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
fail: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Link Checker Summary
if: ${{ !inputs.skip_linkcheck && always() }}
run: |
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 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 "<details>" >> $GITHUB_STEP_SUMMARY
echo "<summary>View broken links</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $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 "</details>" >> $GITHUB_STEP_SUMMARY
elif [ "$TIMEOUTS" -gt 0 ]; then
echo ":warning: **$TIMEOUTS link(s) timed out** (external sites may be slow)" >> $GITHUB_STEP_SUMMARY
else
echo ":white_check_mark: **All links valid**" >> $GITHUB_STEP_SUMMARY
fi
else
echo ":warning: No link check report found." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
- name: Upload link check report
if: ${{ !inputs.skip_linkcheck }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: html-link-report
path: html-link-report.md
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' }}
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710
with:
title: Scheduled Check of Website Content Found Bad Hyperlinks
content-filepath: html-link-report.md
labels: |
bug
documentation
- name: Fail on link check error
# 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