Build and publish Docker images #18
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: Build and publish Docker images | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| releaseTag: | |
| description: 'Required fcli release tag for which to build and release Docker images (e.g., v3.14.0)' | |
| required: true | |
| type: string | |
| doPublish: | |
| description: 'Publish images to Docker Hub' | |
| required: true | |
| type: boolean | |
| default: false | |
| isLatest: | |
| description: 'Tag as latest (set by fcli CI when releasing latest version for current major version)' | |
| required: false | |
| type: boolean | |
| default: false | |
| buildProduction: | |
| description: 'Build production images (scratch, ubi9)' | |
| required: false | |
| type: boolean | |
| default: true | |
| ubiBase: | |
| description: 'Red Hat UBI base image (default: redhat/ubi9:9.7)' | |
| required: false | |
| type: string | |
| default: 'redhat/ubi9:9.7' | |
| buildTest: | |
| description: 'Build test images (alpine, windows)' | |
| required: false | |
| type: boolean | |
| default: false | |
| alpineBase: | |
| description: 'Alpine base image (default: alpine:3.23.0)' | |
| required: false | |
| type: string | |
| default: 'alpine:3.23.0' | |
| servercoreVersions: | |
| description: 'Windows Server Core ltsc versions to build (comma-separated, default: ltsc2022)' | |
| required: false | |
| type: string | |
| default: 'ltsc2022,ltsc2025' | |
| permissions: | |
| contents: read | |
| packages: write | |
| jobs: | |
| check-base-images: | |
| name: Check Base Images | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| include: | |
| - name: Alpine | |
| registry: dockerhub | |
| repo: library/alpine | |
| current: ${{ inputs.alpineBase }} | |
| pattern: '^3\.[0-9]+\.[0-9]+$' | |
| - name: Red Hat UBI9 | |
| registry: dockerhub | |
| repo: redhat/ubi9 | |
| current: ${{ inputs.ubiBase }} | |
| pattern: '^9\.[0-9]+(\.[0-9]+)?$' | |
| - name: Windows Server Core | |
| registry: mcr | |
| repo: windows/servercore | |
| current_versions: ${{ inputs.servercoreVersions }} | |
| pattern: '^ltsc[0-9]+$' | |
| fail-fast: false | |
| steps: | |
| - name: Check ${{ matrix.name }} | |
| continue-on-error: true | |
| run: | | |
| echo "Checking ${{ matrix.name }}..." | |
| CURRENT="${{ matrix.current }}" | |
| PATTERN="${{ matrix.pattern }}" | |
| if [[ "${{ matrix.registry }}" == "dockerhub" ]]; then | |
| REPO="${{ matrix.repo }}" | |
| # Get tags from Docker Hub API | |
| LATEST=$(curl -fsSL "https://registry.hub.docker.com/v2/repositories/${REPO}/tags?page_size=100" 2>/dev/null | \ | |
| jq -r '.results[].name' 2>/dev/null | grep -E "$PATTERN" | sort -V | tail -1) | |
| if [[ -z "$LATEST" ]]; then | |
| echo "::notice::Could not determine latest ${{ matrix.name }} version" | |
| echo "Current: $CURRENT" | |
| else | |
| CURRENT_TAG="${CURRENT##*:}" | |
| if [[ "$LATEST" != "$CURRENT_TAG" ]]; then | |
| echo "::warning::Newer ${{ matrix.name }} version available: $LATEST (current: $CURRENT_TAG)" | |
| else | |
| echo "✓ ${{ matrix.name }} is up to date: $CURRENT" | |
| fi | |
| fi | |
| elif [[ "${{ matrix.registry }}" == "mcr" ]]; then | |
| REPO="${{ matrix.repo }}" | |
| CURRENT_VERSIONS="${{ matrix.current_versions }}" | |
| # MCR API allows anonymous access to tags list | |
| LATEST=$(curl -fsSL "https://mcr.microsoft.com/v2/${REPO}/tags/list" 2>/dev/null | \ | |
| jq -r '.tags[]?' 2>/dev/null | grep -E "$PATTERN" | sort -V | tail -1) | |
| if [[ -z "$LATEST" ]]; then | |
| echo "::notice::Could not determine latest ${{ matrix.name }} version" | |
| echo "Current versions in matrix: $CURRENT_VERSIONS" | |
| else | |
| # Find the latest version in the matrix | |
| IFS=',' read -ra VERSIONS <<< "$CURRENT_VERSIONS" | |
| MATRIX_LATEST=$(printf '%s\n' "${VERSIONS[@]}" | sort -V | tail -1) | |
| if [[ "$LATEST" != "$MATRIX_LATEST" ]]; then | |
| echo "::warning::Newer ${{ matrix.name }} version available: $LATEST (latest in matrix: $MATRIX_LATEST)" | |
| else | |
| echo "✓ ${{ matrix.name }} is up to date: $MATRIX_LATEST" | |
| fi | |
| echo "Matrix includes: $CURRENT_VERSIONS" | |
| fi | |
| fi | |
| generate-metadata: | |
| name: Generate Tag Metadata | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.meta.outputs.version }} | |
| major: ${{ steps.meta.outputs.major }} | |
| minor: ${{ steps.meta.outputs.minor }} | |
| patch: ${{ steps.meta.outputs.patch }} | |
| timestamp: ${{ steps.meta.outputs.timestamp }} | |
| is_semantic: ${{ steps.meta.outputs.is_semantic }} | |
| steps: | |
| - name: Extract metadata | |
| id: meta | |
| run: | | |
| # Extract version from tag (remove 'v' prefix if present) | |
| VERSION="${{ inputs.releaseTag }}" | |
| VERSION="${VERSION#v}" | |
| echo "version=${VERSION}" >> $GITHUB_OUTPUT | |
| # Try to parse semantic version components (x.y.z) | |
| if [[ "$VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then | |
| MAJOR="${BASH_REMATCH[1]}" | |
| MINOR="${BASH_REMATCH[2]}" | |
| PATCH="${BASH_REMATCH[3]}" | |
| echo "major=${MAJOR}" >> $GITHUB_OUTPUT | |
| echo "minor=${MINOR}" >> $GITHUB_OUTPUT | |
| echo "patch=${PATCH}" >> $GITHUB_OUTPUT | |
| echo "is_semantic=true" >> $GITHUB_OUTPUT | |
| # Generate timestamp for semantic versions | |
| TIMESTAMP=$(date +%Y%m%d%H%M%S) | |
| echo "timestamp=${TIMESTAMP}" >> $GITHUB_OUTPUT | |
| echo "Semantic version detected: ${MAJOR}.${MINOR}.${PATCH} (timestamp: ${TIMESTAMP})" | |
| else | |
| echo "is_semantic=false" >> $GITHUB_OUTPUT | |
| echo "Non-semantic version: ${VERSION} (no timestamp or semantic tags)" | |
| fi | |
| generate-linux-matrix: | |
| name: Generate Linux Matrix | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.matrix.outputs.matrix }} | |
| has_variants: ${{ steps.matrix.outputs.has_variants }} | |
| steps: | |
| - name: Generate matrix based on build inputs | |
| id: matrix | |
| run: | | |
| VARIANTS='[]' | |
| # Add production variants if enabled (use -c for compact output) | |
| if [[ "${{ inputs.buildProduction }}" == "true" ]]; then | |
| VARIANTS=$(echo "$VARIANTS" | jq -c '. + [{"name":"scratch","target":"fcli-scratch","suffix":"","latest_suffix":"","base_image":"scratch (statically linked binary)","publish":true}]') | |
| VARIANTS=$(echo "$VARIANTS" | jq -c --arg base "${{ inputs.ubiBase }}" '. + [{"name":"ubi9","target":"fcli-ubi9","suffix":"-ubi9","latest_suffix":"-ubi9","base_image":$base,"publish":true}]') | |
| fi | |
| # Add test variants if enabled | |
| if [[ "${{ inputs.buildTest }}" == "true" ]]; then | |
| VARIANTS=$(echo "$VARIANTS" | jq -c --arg base "${{ inputs.alpineBase }}" '. + [{"name":"alpine","target":"fcli-alpine","suffix":"-alpine","latest_suffix":"-alpine","base_image":$base,"publish":false}]') | |
| fi | |
| # Check if we have any variants to build | |
| VARIANT_COUNT=$(echo "$VARIANTS" | jq 'length') | |
| if [[ "$VARIANT_COUNT" -gt 0 ]]; then | |
| HAS_VARIANTS="true" | |
| else | |
| HAS_VARIANTS="false" | |
| fi | |
| echo "matrix={\"variant\":$VARIANTS}" >> $GITHUB_OUTPUT | |
| echo "has_variants=$HAS_VARIANTS" >> $GITHUB_OUTPUT | |
| echo "Generated Linux matrix with $VARIANT_COUNT variants" | |
| docker-linux: | |
| name: Build & Test Linux Images | |
| needs: [generate-metadata, generate-linux-matrix] | |
| if: ${{ needs.generate-linux-matrix.outputs.has_variants == 'true' }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: ${{ fromJson(needs.generate-linux-matrix.outputs.matrix) }} | |
| env: | |
| DOCKER_SRC: linux | |
| REGISTRY: docker.io | |
| IMAGE_NAME: fortifydocker/fcli | |
| steps: | |
| - name: Check-out source code | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| with: | |
| driver-opts: | | |
| image=moby/buildkit:latest | |
| - name: Docker Login | |
| if: ${{ inputs.doPublish }} | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ secrets.DOCKER_USERNAME }} | |
| password: ${{ secrets.DOCKER_PASSWORD }} | |
| - name: Generate tags for ${{ matrix.variant.name }} | |
| id: tags | |
| run: | | |
| VERSION="${{ needs.generate-metadata.outputs.version }}" | |
| SUFFIX="${{ matrix.variant.suffix }}" | |
| LATEST_SUFFIX="${{ matrix.variant.latest_suffix }}" | |
| if [[ "${{ needs.generate-metadata.outputs.is_semantic }}" == "true" ]]; then | |
| # Semantic version: generate timestamp and semantic tags | |
| TIMESTAMP="${{ needs.generate-metadata.outputs.timestamp }}" | |
| MAJOR="${{ needs.generate-metadata.outputs.major }}" | |
| MINOR="${{ needs.generate-metadata.outputs.minor }}" | |
| # Tags: x.y.z-suffix-timestamp, x.y.z-suffix, x.y-suffix, x-suffix | |
| TAGS="${{ env.IMAGE_NAME }}:${VERSION}${SUFFIX}-${TIMESTAMP}" | |
| TAGS="${TAGS},${{ env.IMAGE_NAME }}:${VERSION}${SUFFIX}" | |
| TAGS="${TAGS},${{ env.IMAGE_NAME }}:${MAJOR}.${MINOR}${SUFFIX}" | |
| TAGS="${TAGS},${{ env.IMAGE_NAME }}:${MAJOR}${SUFFIX}" | |
| # Add 'latest' tag if explicitly requested | |
| if [[ "${{ inputs.isLatest }}" == "true" ]]; then | |
| TAGS="${TAGS},${{ env.IMAGE_NAME }}:latest${LATEST_SUFFIX}" | |
| fi | |
| else | |
| # Non-semantic version (including dev_*): only version tag, no timestamp | |
| TAGS="${{ env.IMAGE_NAME }}:${VERSION}${SUFFIX}" | |
| fi | |
| echo "tags=${TAGS}" >> $GITHUB_OUTPUT | |
| echo "Generated tags for ${{ matrix.variant.name }}: ${TAGS}" | |
| - name: Build and push ${{ matrix.variant.name }} | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: ${{ env.DOCKER_SRC }} | |
| file: ${{ env.DOCKER_SRC }}/Dockerfile | |
| target: ${{ matrix.variant.target }} | |
| platforms: linux/amd64 | |
| push: ${{ matrix.variant.publish && inputs.doPublish }} | |
| tags: ${{ steps.tags.outputs.tags }} | |
| labels: | | |
| org.opencontainers.image.source=${{ github.repositoryUrl }} | |
| org.opencontainers.image.revision=${{ github.sha }} | |
| org.opencontainers.image.created=${{ github.event.repository.updated_at }} | |
| build-args: | | |
| FCLI_VERSION=${{ inputs.releaseTag }} | |
| ALPINE_BASE=${{ inputs.alpineBase }} | |
| UBI_BASE=${{ inputs.ubiBase }} | |
| provenance: ${{ matrix.variant.publish }} | |
| sbom: ${{ matrix.variant.publish }} | |
| load: ${{ !matrix.variant.publish }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Test ${{ matrix.variant.name }} image | |
| run: | | |
| VERSION="${{ needs.generate-metadata.outputs.version }}" | |
| SUFFIX="${{ matrix.variant.suffix }}" | |
| TAG="${{ env.IMAGE_NAME }}:${VERSION}${SUFFIX}" | |
| TEST_DIR="${PWD}/test-${{ matrix.variant.name }}" | |
| # Try to pull image if published, otherwise use local | |
| docker pull ${TAG} 2>/dev/null || \ | |
| docker tag $(docker images -q ${TAG} | head -1) ${TAG} 2>/dev/null || true | |
| mkdir -p ${TEST_DIR} | |
| # Determine command prefix based on variant | |
| if [[ "${{ matrix.variant.name }}" == "scratch" ]]; then | |
| CMD_PREFIX="" | |
| else | |
| CMD_PREFIX="fcli" | |
| fi | |
| docker run --rm -u $(id -u):$(id -g) -v ${TEST_DIR}:/data \ | |
| ${TAG} ${CMD_PREFIX} tool sc-client install | |
| test -f ${TEST_DIR}/fortify/tools/bin/scancentral | |
| echo "✓ ${{ matrix.variant.name }} image test passed" | |
| - name: Generate summary for ${{ matrix.variant.name }} | |
| if: always() | |
| run: | | |
| # Determine published status | |
| PUBLISHED="${{ matrix.variant.publish && inputs.doPublish }}" | |
| if [[ "${PUBLISHED}" == "true" ]]; then | |
| PUBLISH_STATUS="✓ Published" | |
| else | |
| PUBLISH_STATUS="✗ Not Published (test only)" | |
| fi | |
| echo "### ${{ matrix.variant.name }} image" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Base Image:** \`${{ matrix.variant.base_image }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Target:** \`${{ matrix.variant.target }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Status:** ${PUBLISH_STATUS}" >> $GITHUB_STEP_SUMMARY | |
| # Show generated tags if variant is publishable | |
| if [[ "${{ matrix.variant.publish }}" == "true" ]]; then | |
| echo "- **Tags:**" >> $GITHUB_STEP_SUMMARY | |
| # Parse and display the generated tags (from steps.tags.outputs.tags) | |
| TAGS="${{ steps.tags.outputs.tags }}" | |
| IFS=',' read -ra TAG_ARRAY <<< "$TAGS" | |
| for tag in "${TAG_ARRAY[@]}"; do | |
| # Mark timestamp tags as immutable | |
| if [[ "$tag" =~ -[0-9]{14}$ ]]; then | |
| echo " - \`${tag}\` (immutable)" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo " - \`${tag}\`" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| done | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| generate-windows-matrix: | |
| name: Generate Windows Matrix | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.matrix.outputs.matrix }} | |
| steps: | |
| - name: Generate matrix from servercoreVersions input | |
| id: matrix | |
| run: | | |
| VERSIONS="${{ inputs.servercoreVersions }}" | |
| # Convert comma-separated string to JSON array (compact output) | |
| # e.g., "ltsc2022,ltsc2025" -> ["ltsc2022","ltsc2025"] | |
| JSON_ARRAY=$(echo "$VERSIONS" | jq -Rc 'split(",") | map(gsub("^\\s+|\\s+$";""))') | |
| echo "matrix={\"ltsc\":$JSON_ARRAY}" >> $GITHUB_OUTPUT | |
| echo "Generated Windows matrix: $JSON_ARRAY" | |
| # Windows images: build only for testing, do not publish | |
| docker-windows: | |
| name: Build Windows Images (Test Only) | |
| needs: [generate-metadata, generate-windows-matrix] | |
| if: ${{ inputs.buildTest }} | |
| runs-on: windows-${{ matrix.ltsc }} | |
| strategy: | |
| matrix: ${{ fromJson(needs.generate-windows-matrix.outputs.matrix) }} | |
| fail-fast: false | |
| env: | |
| DOCKER_SRC: windows | |
| steps: | |
| - name: Check-out source code | |
| uses: actions/checkout@v4 | |
| - name: Build Windows image for ${{ matrix.ltsc }} | |
| shell: pwsh | |
| run: | | |
| cd $env:DOCKER_SRC | |
| $ltscVersion = "${{ matrix.ltsc }}" | |
| $baseImage = "mcr.microsoft.com/windows/servercore:$ltscVersion" | |
| $targetName = "fcli-$ltscVersion" | |
| docker build . ` | |
| --target $targetName ` | |
| -t fcli-windows:$ltscVersion ` | |
| --build-arg FCLI_VERSION=${{ inputs.releaseTag }} ` | |
| --build-arg SERVERCORE_BASE=$baseImage | |
| Write-Host "✓ Windows $ltscVersion image build completed" | |
| - name: Test Windows ${{ matrix.ltsc }} image | |
| shell: pwsh | |
| run: | | |
| $ltscVersion = "${{ matrix.ltsc }}" | |
| # Basic test: check fcli version | |
| docker run --rm fcli-windows:$ltscVersion fcli --version | |
| Write-Host "✓ Windows $ltscVersion image test passed" | |
| Write-Host "Note: Windows images are built for testing only and are not published" | |
| - name: Summary for ${{ matrix.ltsc }} | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $ltscVersion = "${{ matrix.ltsc }}" | |
| $baseImage = "mcr.microsoft.com/windows/servercore:$ltscVersion" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### windows-$ltscVersion image" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- **Base Image:** ``$baseImage``" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- **Target:** ``fcli-$ltscVersion``" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- **Runner:** ``windows-$ltscVersion``" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- **Status:** ✗ Not Published (test only)" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "Windows images are built for testing only and are not published to Docker Hub." | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "" |