Build and publish Docker images #13
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 | |
| alpineBase: | |
| description: 'Alpine base image (default: alpine:3.23.0)' | |
| required: false | |
| type: string | |
| default: 'alpine:3.23.0' | |
| ubiBase: | |
| description: 'Red Hat UBI base image (default: redhat/ubi9:9.7)' | |
| required: false | |
| type: string | |
| default: 'redhat/ubi9:9.7' | |
| servercoreVersions: | |
| description: 'Windows Server Core ltsc versions to build (comma-separated, default: ltsc2022)' | |
| required: false | |
| type: string | |
| default: 'ltsc2022,ltsc2025' | |
| isLatest: | |
| description: 'Tag as latest (set by fcli CI when releasing latest version for current major version)' | |
| required: false | |
| type: boolean | |
| default: false | |
| 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 | |
| docker-linux: | |
| name: Build & Test Linux Images | |
| needs: generate-metadata | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| variant: | |
| - name: scratch | |
| target: fcli-scratch | |
| suffix: '' | |
| latest_suffix: '' | |
| base_image: 'scratch (statically linked binary)' | |
| publish: true | |
| - name: ubi9 | |
| target: fcli-ubi9 | |
| suffix: -ubi9 | |
| latest_suffix: -ubi9 | |
| base_image: ${{ inputs.ubiBase }} | |
| publish: true | |
| - name: alpine | |
| target: fcli-alpine | |
| suffix: -alpine | |
| latest_suffix: -alpine | |
| base_image: ${{ inputs.alpineBase }} | |
| publish: false | |
| 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 | |
| # e.g., "ltsc2022,ltsc2025" -> ["ltsc2022","ltsc2025"] | |
| JSON_ARRAY=$(echo "$VERSIONS" | jq -R '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] | |
| 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 "" |