Build and publish Docker images #12
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' | |
| servercoreBase: | |
| description: 'Windows Server Core base image (default: mcr.microsoft.com/windows/servercore:ltsc2025)' | |
| required: false | |
| type: string | |
| default: 'mcr.microsoft.com/windows/servercore: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: ${{ inputs.servercoreBase }} | |
| 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 }}" | |
| # 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: $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 | |
| 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 | |
| # Windows images: build only for testing, do not publish | |
| docker-windows: | |
| name: Build Windows Images (Test Only) | |
| needs: generate-metadata | |
| runs-on: windows-2022 | |
| env: | |
| DOCKER_SRC: windows | |
| steps: | |
| - name: Check-out source code | |
| uses: actions/checkout@v4 | |
| - name: Build Windows image | |
| shell: pwsh | |
| run: | | |
| cd $env:DOCKER_SRC | |
| docker build . ` | |
| --target fcli-ltsc2022 ` | |
| -t fcli-windows:test ` | |
| --build-arg FCLI_VERSION=${{ inputs.releaseTag }} ` | |
| --build-arg SERVERCORE_BASE=${{ inputs.servercoreBase }} | |
| Write-Host "✓ Windows image build completed" | |
| - name: Test Windows image | |
| shell: pwsh | |
| run: | | |
| # Basic test: check fcli version | |
| docker run --rm fcli-windows:test fcli --version | |
| Write-Host "✓ Windows image test passed" | |
| Write-Host "Note: Windows images are built for testing only and are not published" | |
| - name: Summary | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### windows image" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- **Base Image:** ``${{ inputs.servercoreBase }}``" | |
| Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "- **Target:** ``fcli-ltsc2022``" | |
| 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 "" |