Skip to content

Create Docker Release #16

Create Docker Release

Create Docker Release #16

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 }}"