-
Notifications
You must be signed in to change notification settings - Fork 0
415 lines (354 loc) · 19.5 KB
/
docker-release.yml
File metadata and controls
415 lines (354 loc) · 19.5 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
name: Create Docker Release
on:
workflow_dispatch:
inputs:
version_bump:
description: 'Version bump type (only used for main branch releases)'
required: false
default: 'patch'
type: choice
options:
- major
- minor
- patch
env:
GIT_USER_NAME: ${{ vars.GIT_USER_NAME || 'GitHub Pipeline' }}
GIT_USER_EMAIL: ${{ vars.GIT_USER_EMAIL || 'github_pipeline@fairagro.net' }}
IMAGE_BASE_NAME: ${{ vars.IMAGE_BASE_NAME || 'fairagro-advanced-middleware' }}
DOCKERHUB_NAMESPACE: ${{ vars.DOCKERHUB_NAMESPACE || 'zalf' }}
IMAGE_TITLE: ${{ vars.IMAGE_TITLE || 'FAIRagro SQL-to-ARC' }}
IMAGE_DESCRIPTION: ${{ vars.IMAGE_DESCRIPTION || 'Middleware for converting SQL metadata to ARC format for FAIRagro' }}
# Derived static variables
GITVERSION_TAG_PREFIX: ${{ vars.GITVERSION_TAG_PREFIX || '.*-docker-v' }}
DOCKER_PLATFORMS: ${{ vars.DOCKER_PLATFORMS || 'linux/amd64' }}
# Conditional variables using expressions - SINGLE SOURCE OF TRUTH
IS_FEATURE_BRANCH: ${{ startsWith(github.ref_name, 'feature/') }}
IS_MAIN_BRANCH: ${{ github.ref_name == 'main' }}
RELEASE_TYPE: ${{ startsWith(github.ref_name, 'feature/') && 'feature' || 'final' }}
VERSION_INCREMENT: ${{ github.event.inputs.version_bump || 'patch' }}
CREATE_GITHUB_RELEASE: ${{ github.ref_name == 'main' }}
jobs:
docker-build-test:
name: Docker Build and Test
permissions:
contents: read
uses: ./.github/workflows/docker-build.yml
with:
push_to_registry: true
version_bump: ${{ startsWith(github.ref_name, 'feature/') && 'patch' || (github.event.inputs.version_bump || 'patch') }}
components: '["sql_to_arc"]' # Add other components here in the future, e.g., '["worker"]'
secrets: inherit
security-scan-release:
needs: docker-build-test
strategy:
matrix:
component: ${{ fromJson(needs.docker-build-test.outputs.components) }}
runs-on: ubuntu-latest
if: needs.docker-build-test.result == 'success'
permissions:
contents: read
security-events: write # Required for SARIF upload
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download Docker image artifact
uses: actions/download-artifact@v4
with:
name: docker-image-${{ matrix.component }}-${{ needs.docker-build-test.outputs.version }}
- name: Load Docker image
run: |
docker load < docker-image-${{ matrix.component }}.tar.gz
# Determine the correct image tag that was saved
if [[ "${{ needs.docker-build-test.outputs.dockerhub-pushed }}" == "true" ]]; then
IMAGE_TAG="${{ env.DOCKERHUB_NAMESPACE }}/${{ env.IMAGE_BASE_NAME }}-${{ matrix.component }}:${{ needs.docker-build-test.outputs.version }}"
echo "✅ Docker image loaded from DockerHub: $IMAGE_TAG"
else
IMAGE_TAG="local/${{ env.IMAGE_BASE_NAME }}-${{ matrix.component }}:${{ needs.docker-build-test.outputs.version }}"
echo "✅ Docker image loaded locally: $IMAGE_TAG"
fi
# Export for next steps
echo "SBOM_IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
- name: Create SBOM directory
run: mkdir -p sboms
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ${{ env.SBOM_IMAGE_TAG }}
output-file: sboms/sbom-${{ matrix.component }}.spdx.json
format: spdx-json
- name: Analyze SBOM completeness
run: |
echo "## 📋 SBOM Analysis for Python/uv Application" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Count packages in SBOM
SBOM_FILE="sboms/sbom-${{ matrix.component }}.spdx.json"
SPDX_PACKAGES=$(jq '.packages | length' "$SBOM_FILE" 2>/dev/null || echo "0")
echo "### 📊 Package Count" >> $GITHUB_STEP_SUMMARY
echo "- **SPDX SBOM**: $SPDX_PACKAGES packages detected" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Analyze package types specifically for Python
if [[ -f "$SBOM_FILE" ]]; then
echo "### 🐍 Python Production Dependencies" >> $GITHUB_STEP_SUMMARY
echo "**Note**: Only production dependencies should be present (no pytest, pylint, etc.)" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
jq -r '.packages[] | select(.name != null and (.name | test("python|py|flask|werkzeug|jinja|click"; "i"))) | "\(.name) (\(.versionInfo // "unknown"))"' "$SBOM_FILE" | head -15 >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📦 System Packages (Debian/APT)" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
jq -r '.packages[] | select(.name != null and (.name | test("lib|curl|apt|deb"; "i"))) | "\(.name) (\(.versionInfo // "unknown"))"' "$SBOM_FILE" | head -10 >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
- name: Security scan with Trivy (Image)
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.SBOM_IMAGE_TAG }}
format: sarif
output: trivy-image-results-${{ matrix.component }}.sarif
severity: 'CRITICAL,HIGH,MEDIUM' # Skip LOW severity to reduce noise
ignore-unfixed: true # Focus on fixable vulnerabilities
- name: Security scan with Trivy (SBOM)
uses: aquasecurity/trivy-action@master
with:
scan-type: sbom
input: sboms/sbom-${{ matrix.component }}.spdx.json
format: sarif
output: trivy-sbom-results-${{ matrix.component }}.sarif
scan-ref: sboms/sbom-${{ matrix.component }}.spdx.json
severity: 'CRITICAL,HIGH,MEDIUM' # Skip LOW severity to reduce noise
ignore-unfixed: true # Focus on fixable vulnerabilities
- name: Upload Trivy Image SARIF results
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: trivy-image-results-${{ matrix.component }}.sarif
category: trivy-image-scan-${{ matrix.component }}
- name: Upload Trivy SBOM SARIF results
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: trivy-sbom-results-${{ matrix.component }}.sarif
category: trivy-sbom-scan-${{ matrix.component }}
continue-on-error: true
- name: License Compliance Summary
run: |
echo "## ⚖️ License Compliance Analysis" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Your Project License**: MIT (very permissive)" >> $GITHUB_STEP_SUMMARY
echo "**Compatibility Strategy**: MIT allows mixing with most licenses" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Quick license summary from SBOM
echo "### 📊 Detected Licenses in Dependencies" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
jq -r '.packages[] | select(.licenseConcluded != null and .licenseConcluded != "NOASSERTION" and .licenseConcluded != "") | .licenseConcluded' "sboms/sbom-${{ matrix.component }}.spdx.json" | sort | uniq -c | sort -nr >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Check for copyleft licenses that might require attention
COPYLEFT=$(jq -r '.packages[] | select(.licenseConcluded != null and (.licenseConcluded | test("GPL|LGPL|AGPL|CDDL|EPL|MPL"; "i"))) | "\(.name): \(.licenseConcluded)"' "sboms/sbom-${{ matrix.component }}.spdx.json")
if [[ -n "$COPYLEFT" && "$COPYLEFT" != "" ]]; then
echo "⚠️ **Copyleft Licenses Detected** (review recommended):" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$COPYLEFT" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "**Note**: These may require derivative works to use the same license" >> $GITHUB_STEP_SUMMARY
else
echo "✅ **No problematic copyleft licenses detected**" >> $GITHUB_STEP_SUMMARY
fi
# Count non-standard licenses (LicenseRef-*)
LICENSEREF_COUNT=$(jq -r '.packages[] | select(.licenseConcluded != null and (.licenseConcluded | startswith("LicenseRef-"))) | .licenseConcluded' "sboms/sbom-${{ matrix.component }}.spdx.json" | wc -l)
if [[ "$LICENSEREF_COUNT" -gt 0 ]]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "📄 **Non-standard licenses**: $LICENSEREF_COUNT (typically system libraries - usually safe)" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📋 MIT License Compatibility Reference" >> $GITHUB_STEP_SUMMARY
echo "**Note**: This is a general reference - actual issues are reported above if found." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **✅ Always Compatible**: Apache-2.0, BSD variants, ISC, CC0-1.0" >> $GITHUB_STEP_SUMMARY
echo "- **⚠️ Would Need Review**: GPL variants, LGPL, AGPL, CDDL, EPL, MPL" >> $GITHUB_STEP_SUMMARY
echo "- **✅ System Libraries**: LicenseRef-* (Debian packages - safe for distribution)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "💡 **MIT allows almost everything** - you can include dependencies with any license in binary distributions" >> $GITHUB_STEP_SUMMARY
- name: Security Scan Summary
run: |
echo "## 🔒 Security Scan Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Scanning Strategy**: Using Trivy for comprehensive vulnerability detection" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Parse Trivy Image results (Image-based)
echo "### 🐳 Trivy Image Scan" >> $GITHUB_STEP_SUMMARY
if [[ -f "trivy-image-results-${{ matrix.component }}.sarif" ]]; then
TRIVY_IMAGE_RESULTS=$(jq '.runs[0].results | length' trivy-image-results-${{ matrix.component }}.sarif 2>/dev/null || echo "0")
TRIVY_IMAGE_CRITICAL=$(jq '.runs[0].results[] | select(.level == "error") | length' trivy-image-results-${{ matrix.component }}.sarif 2>/dev/null || echo "0")
echo "- **Total findings**: $TRIVY_IMAGE_RESULTS" >> $GITHUB_STEP_SUMMARY
echo "- **Critical/High**: $TRIVY_IMAGE_CRITICAL" >> $GITHUB_STEP_SUMMARY
echo "- **Coverage**: Direct image layer analysis, OS packages, secrets, misconfigurations" >> $GITHUB_STEP_SUMMARY
else
echo "- ❌ No Trivy Image results available" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
# Parse Trivy SBOM results (SBOM-based)
echo "### 📋 Trivy SBOM Scan" >> $GITHUB_STEP_SUMMARY
if [[ -f "trivy-sbom-results-${{ matrix.component }}.sarif" ]]; then
TRIVY_SBOM_RESULTS=$(jq '.runs[0].results | length' trivy-sbom-results-${{ matrix.component }}.sarif 2>/dev/null || echo "0")
TRIVY_SBOM_CRITICAL=$(jq '.runs[0].results[] | select(.level == "error") | length' trivy-sbom-results-${{ matrix.component }}.sarif 2>/dev/null || echo "0")
echo "- **Total findings**: $TRIVY_SBOM_RESULTS" >> $GITHUB_STEP_SUMMARY
echo "- **Critical/High**: $TRIVY_SBOM_CRITICAL" >> $GITHUB_STEP_SUMMARY
echo "- **Coverage**: SBOM-based vulnerability detection with Trivy's curated database" >> $GITHUB_STEP_SUMMARY
else
echo "- ❌ No Trivy SBOM results available" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🔍 Trivy Scan Comparison" >> $GITHUB_STEP_SUMMARY
if [[ -f "trivy-image-results-${{ matrix.component }}.sarif" && -f "trivy-sbom-results-${{ matrix.component }}.sarif" ]]; then
IMAGE_FINDINGS=$(jq '.runs[0].results | length' trivy-image-results-${{ matrix.component }}.sarif 2>/dev/null || echo "0")
SBOM_FINDINGS=$(jq '.runs[0].results | length' trivy-sbom-results-${{ matrix.component }}.sarif 2>/dev/null || echo "0")
echo "- **Image Scan**: $IMAGE_FINDINGS findings" >> $GITHUB_STEP_SUMMARY
echo "- **SBOM Scan**: $SBOM_FINDINGS findings" >> $GITHUB_STEP_SUMMARY
if [[ "$IMAGE_FINDINGS" -eq "$SBOM_FINDINGS" ]]; then
echo "- **Result**: ✅ Both scans found identical number of issues" >> $GITHUB_STEP_SUMMARY
elif [[ "$IMAGE_FINDINGS" -gt "$SBOM_FINDINGS" ]]; then
DIFF=$((IMAGE_FINDINGS - SBOM_FINDINGS))
echo "- **Result**: 🔍 Image scan found $DIFF additional issues (layer analysis, configs, secrets)" >> $GITHUB_STEP_SUMMARY
else
DIFF=$((SBOM_FINDINGS - IMAGE_FINDINGS))
echo "- **Result**: 📋 SBOM scan found $DIFF additional issues (dependency-focused analysis)" >> $GITHUB_STEP_SUMMARY
fi
# Compare CVE overlaps if both have results
if [[ "$IMAGE_FINDINGS" -gt 0 && "$SBOM_FINDINGS" -gt 0 ]]; then
echo "- **Analysis**: Both scanning methods complement each other" >> $GITHUB_STEP_SUMMARY
fi
else
echo "- **Result**: ⚠️ Cannot compare - one or both scan results missing" >> $GITHUB_STEP_SUMMARY
fi
echo "### 📋 Scan Results Available In:" >> $GITHUB_STEP_SUMMARY
echo "- **GitHub Security Tab**: SARIF results for both scan types" >> $GITHUB_STEP_SUMMARY
echo "- **Artifacts**: Full SARIF reports attached to workflow run" >> $GITHUB_STEP_SUMMARY
- name: Upload SBOMs as a single artifact
uses: actions/upload-artifact@v4
with:
name: sboms-${{ matrix.component }}
path: sboms/
create-release:
needs: [docker-build-test, security-scan-release]
runs-on: ubuntu-latest
if: needs.docker-build-test.result == 'success' && needs.security-scan-release.result == 'success'
permissions:
contents: write
env:
TIMESTAMP: ${{ github.run_id }}${{ github.run_attempt }}
RELEASE_TAG: ${{ github.run_id }}${{ github.run_attempt }}-docker-v${{ needs.docker-build-test.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download SBOM artifact (final releases only)
if: env.RELEASE_TYPE == 'final'
uses: actions/download-artifact@v4
with:
pattern: sboms-*
path: sboms/
merge-multiple: true
- name: Create version tag
id: create_tag
uses: mathieudutour/github-tag-action@v6.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
custom_tag: ${{ env.RELEASE_TAG }}
tag_prefix: ""
create_annotated_tag: true
- name: Log release info
run: |
if [[ "${{ env.RELEASE_TYPE }}" == "final" ]]; then
echo "Creating GitHub release for final release (main branch)"
echo "✅ Git tag: ${{ env.RELEASE_TAG }}"
echo "✅ GitHub release: Will be created"
else
echo "Feature branch release - tracking version progression"
echo "✅ Git tag: ${{ env.RELEASE_TAG }} (for GitVersion tracking)"
echo "⏭️ GitHub release: Skipped (feature branch)"
echo "💡 This tag helps GitVersion increment properly on subsequent builds"
fi
- name: Create GitHub Release (Draft)
if: env.RELEASE_TYPE == 'final'
id: draft_release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.RELEASE_TAG }}
name: "docker-v${{ needs.docker-build-test.outputs.version }}"
body: |
## Docker Release v${{ needs.docker-build-test.outputs.version }}
**Docker Images**:
- `${{ env.DOCKERHUB_NAMESPACE }}/${{ env.IMAGE_BASE_NAME }}-sql_to_arc:${{ needs.docker-build-test.outputs.version }}`
**Image Digest**: `${{ needs.docker-build-test.outputs.image-digest }}`
- ✅ Security scan completed (see Security tab)
- ✅ SBOMs attached
- 🐳 Platform: linux/amd64
### Installation
${{ needs.docker-build-test.outputs.dockerhub-pushed == 'true' && '```bash
# Pull and run the Docker image
docker pull ' || '⚠️ **Image not available on DockerHub** (missing credentials)
The Docker image was built locally but not pushed to DockerHub.
You can build it yourself:
```bash
# Build the image locally
git clone https://github.com/' }}${{ needs.docker-build-test.outputs.dockerhub-pushed == 'true' && format('{0}/{1}-sql_to_arc:{2}', env.DOCKERHUB_NAMESPACE, env.IMAGE_BASE_NAME, needs.docker-build-test.outputs.version) || format('{0}
cd {1}
git checkout {2}
docker build -t local/{3}-sql_to_arc:{4} -f docker/Dockerfile.sql_to_arc .', github.repository, github.repository, github.sha, env.IMAGE_BASE_NAME, needs.docker-build-test.outputs.version) }}
```
files: |
sboms/*
draft: true
make_latest: true
generate_release_notes: true
append_body: true
fail_on_unmatched_files: true
- name: Finalize Release
if: env.RELEASE_TYPE == 'final'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "🔍 Looking for draft release with tag: ${{ env.RELEASE_TAG }}"
# Give GitHub API a moment to index the new release
sleep 5
# Try to get the release ID from the draft release we just created
if RELEASE_ID=$(gh api repos/${{ github.repository }}/releases/tags/${{ env.RELEASE_TAG }} --jq '.id' 2>/dev/null); then
echo "✅ Found release ID via tag: $RELEASE_ID"
else
echo "⚠️ Release not found via tag, searching in all releases..."
# Search for the release by version in all releases
RELEASE_ID=$(gh api repos/${{ github.repository }}/releases --jq '.[] | select(.tag_name == "${{ env.RELEASE_TAG }}") | .id')
if [[ -n "$RELEASE_ID" ]]; then
echo "✅ Found release ID via search: $RELEASE_ID"
else
echo "❌ Release not found at all!"
exit 1
fi
fi
# Convert draft to final release
gh api repos/${{ github.repository }}/releases/$RELEASE_ID \
--method PATCH \
--field draft=false
echo "🎉 Release finalized successfully"
# currently there is no helm chart to update
# update-helm-chart:
# needs: [docker-build-test, security-scan-release]
# runs-on: ubuntu-latest
# if: needs.docker-build-test.outputs.dockerhub-pushed == 'true' && needs.security-scan-release.result == 'success'
# permissions:
# contents: write
# steps:
# - uses: actions/checkout@v4
# with:
# fetch-depth: 0
# - name: Update Chart.yaml appVersion
# run: |
# sed -i "s/^appVersion:.*/appVersion: \"${{ needs.docker-build-test.outputs.version }}\"/" helm/Chart.yaml
# - name: Commit and push Chart.yaml changes
# uses: stefanzweifel/git-auto-commit-action@v5
# with:
# commit_message: "chore(helm): update appVersion to ${{ needs.docker-build-test.outputs.version }}"
# file_pattern: "helm/Chart.yaml"
# commit_user_name: "${{ env.GIT_USER_NAME }}"
# commit_user_email: "${{ env.GIT_USER_EMAIL }}"