Create Docker Release #16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }}" |