Container Build #1412
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: Container Build | |
| env: | |
| CONTAINER_TRIGGERS: "/container" | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| paths: | |
| - 'Dockerfile*' | |
| - 'src/**' | |
| - 'pyproject.toml' | |
| - 'deployment/docker/**' | |
| - 'dev-tools/container/container_build.sh' | |
| - '.github/workflows/dev-containers.yml' | |
| workflow_run: | |
| workflows: ["Quality Checks", "Unit Tests"] | |
| types: [completed] | |
| branches: [main] | |
| pull_request: | |
| branches: [ main, develop ] | |
| paths: | |
| - 'Dockerfile*' | |
| - 'src/**' | |
| - 'pyproject.toml' | |
| - 'deployment/docker/**' | |
| - 'dev-tools/container/container_build.sh' | |
| - '.github/workflows/dev-containers.yml' | |
| workflow_dispatch: | |
| inputs: | |
| push_images: | |
| description: 'Push images to registry' | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: read | |
| packages: read | |
| concurrency: | |
| group: "container-registry" | |
| cancel-in-progress: true | |
| jobs: | |
| config: | |
| name: Configuration | |
| uses: ./.github/workflows/shared-config.yml | |
| if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' | |
| calculate-tags: | |
| name: Calculate Container Tags | |
| runs-on: ubuntu-latest | |
| needs: config | |
| permissions: | |
| contents: read | |
| outputs: | |
| primary-tags: ${{ steps.tags.outputs.primary-tags }} | |
| python-tags: ${{ steps.tags.outputs.python-tags }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Calculate container tags | |
| id: tags | |
| env: | |
| IS_RELEASE: ${{ needs.config.outputs.is-release }} | |
| run: | | |
| make get-container-tags >> "$GITHUB_OUTPUT" | |
| setup-cache: | |
| name: Setup Build Cache | |
| needs: config | |
| uses: ./.github/workflows/cache-management.yml | |
| with: | |
| cache-type: build | |
| cache-key-base: container-build | |
| python-version: ${{ needs.config.outputs.default-python-version }} | |
| container-pipeline: | |
| name: Build and Scan Container Images | |
| runs-on: ubuntu-latest | |
| needs: [config, calculate-tags, setup-cache] | |
| strategy: | |
| matrix: | |
| python-version: ${{ fromJSON(needs.config.outputs.python-versions) }} | |
| env: | |
| SCAN_LEVEL: ${{ matrix.python-version == needs.config.outputs.default-python-version && 'full' || 'basic' }} | |
| PUSH_TO_REGISTRY: true | |
| permissions: | |
| contents: read | |
| packages: write | |
| security-events: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 | |
| with: | |
| platforms: linux/amd64,linux/arm64 | |
| - name: Log in to Container Registry | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 | |
| with: | |
| registry: ${{ needs.config.outputs.container-registry }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Python and UV | |
| uses: ./.github/actions/setup-uv-cached | |
| with: | |
| cache-key: ${{ needs.setup-cache.outputs.cache-key }} | |
| fail-on-cache-miss: false | |
| - name: Build package wheel | |
| run: make build | |
| - name: Build and scan container image | |
| env: | |
| REGISTRY: ${{ needs.config.outputs.container-registry }} | |
| IMAGE_NAME: ${{ needs.config.outputs.container-image }} | |
| VERSION: ${{ needs.config.outputs.package-version }} | |
| PYTHON_VERSION: ${{ matrix.python-version }} | |
| run: | | |
| if [[ "${{ env.PUSH_TO_REGISTRY }}" == "true" && "${{ github.event_name }}" != "pull_request" ]]; then | |
| # Build multi-platform image and push to registry | |
| docker buildx build \ | |
| --platform linux/amd64,linux/arm64 \ | |
| --build-arg "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ | |
| --build-arg "VERSION=$VERSION" \ | |
| --build-arg "VCS_REF=$(git rev-parse --short HEAD)" \ | |
| --build-arg "PYTHON_VERSION=$PYTHON_VERSION" \ | |
| --build-arg PACKAGE_NAME_SHORT=orb \ | |
| --cache-from type=gha \ | |
| --cache-to type=gha,mode=max \ | |
| --push \ | |
| -t "$REGISTRY/$IMAGE_NAME:$VERSION-python$PYTHON_VERSION" \ | |
| . | |
| # Pull back amd64 version for local scanning | |
| docker pull --platform linux/amd64 "$REGISTRY/$IMAGE_NAME:$VERSION-python$PYTHON_VERSION" | |
| else | |
| # Build single platform for scanning only (PR/test builds) | |
| docker buildx build \ | |
| --platform linux/amd64 \ | |
| --build-arg "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ | |
| --build-arg "VERSION=$VERSION" \ | |
| --build-arg "VCS_REF=$(git rev-parse --short HEAD)" \ | |
| --build-arg "PYTHON_VERSION=$PYTHON_VERSION" \ | |
| --build-arg PACKAGE_NAME_SHORT=orb \ | |
| --cache-from type=gha \ | |
| --cache-to type=gha,mode=max \ | |
| --load \ | |
| -t "$REGISTRY/$IMAGE_NAME:$VERSION-python$PYTHON_VERSION" \ | |
| . | |
| fi | |
| - name: Run Trivy security scan | |
| uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 | |
| with: | |
| image-ref: "${{ needs.config.outputs.container-registry }}/${{ needs.config.outputs.container-image }}:${{ needs.config.outputs.package-version }}-python${{ matrix.python-version }}" | |
| format: 'sarif' | |
| output: 'trivy-results-py${{ matrix.python-version }}.sarif' | |
| exit-code: '0' | |
| severity: ${{ env.SCAN_LEVEL == 'full' && 'CRITICAL,HIGH,MEDIUM' || 'CRITICAL,HIGH' }} | |
| - name: Upload Trivy scan results | |
| uses: github/codeql-action/upload-sarif@dc73d59c2d7bd4f8194098a91219eeee6d8a1719 # v4 | |
| if: always() | |
| with: | |
| sarif_file: 'trivy-results-py${{ matrix.python-version }}.sarif' | |
| - name: Run Hadolint Dockerfile scan | |
| if: matrix.python-version == needs.config.outputs.default-python-version | |
| uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 | |
| with: | |
| dockerfile: Dockerfile | |
| format: sarif | |
| output-file: hadolint-dockerfile.sarif | |
| no-fail: true | |
| - name: Upload Hadolint scan results | |
| if: matrix.python-version == needs.config.outputs.default-python-version | |
| uses: github/codeql-action/upload-sarif@dc73d59c2d7bd4f8194098a91219eeee6d8a1719 # v4 | |
| with: | |
| sarif_file: hadolint-dockerfile.sarif | |
| container-manifest: | |
| name: Create Multi-Architecture Manifests | |
| runs-on: ubuntu-latest | |
| needs: [config, calculate-tags, setup-cache, container-pipeline] | |
| if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.event_name == 'release') | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Log in to Container Registry | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 | |
| with: | |
| registry: ${{ needs.config.outputs.container-registry }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Create primary tag manifests | |
| env: | |
| REGISTRY: ${{ needs.config.outputs.container-registry }} | |
| IMAGE: ${{ needs.config.outputs.container-image }} | |
| VERSION: ${{ needs.config.outputs.package-version }} | |
| DEFAULT_PYTHON: ${{ needs.config.outputs.default-python-version }} | |
| PRIMARY_TAGS: ${{ needs.calculate-tags.outputs.primary-tags }} | |
| PYTHON_TAGS: ${{ needs.calculate-tags.outputs.python-tags }} | |
| run: | | |
| # Create primary tags (latest, X.Y.Z) -> point to default Python | |
| if [[ -n "$PRIMARY_TAGS" ]]; then | |
| IFS=',' read -ra TAGS <<< "$PRIMARY_TAGS" | |
| for tag in "${TAGS[@]}"; do | |
| echo "Creating primary tag: $tag -> $VERSION-python$DEFAULT_PYTHON" | |
| docker buildx imagetools create \ | |
| -t "$REGISTRY/$IMAGE:$tag" \ | |
| "$REGISTRY/$IMAGE:$VERSION-python$DEFAULT_PYTHON" | |
| done | |
| fi | |
| # Create Python variant tags (pythonX.Y) -> point to specific Python latest | |
| if [[ -n "$PYTHON_TAGS" ]]; then | |
| IFS=',' read -ra TAGS <<< "$PYTHON_TAGS" | |
| for tag in "${TAGS[@]}"; do | |
| py_ver=${tag#python} | |
| echo "Creating Python variant tag: $tag -> $VERSION-python$py_ver" | |
| docker buildx imagetools create \ | |
| -t "$REGISTRY/$IMAGE:$tag" \ | |
| "$REGISTRY/$IMAGE:$VERSION-python$py_ver" | |
| done | |
| fi | |
| container-validation: | |
| name: Validate Container Tags | |
| runs-on: ubuntu-latest | |
| needs: [config, calculate-tags, setup-cache, container-manifest] | |
| if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.event_name == 'release') | |
| permissions: | |
| contents: read | |
| packages: read | |
| steps: | |
| - name: Log in to Container Registry | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 | |
| with: | |
| registry: ${{ needs.config.outputs.container-registry }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Test primary tags | |
| env: | |
| REGISTRY: ${{ needs.config.outputs.container-registry }} | |
| IMAGE: ${{ needs.config.outputs.container-image }} | |
| PRIMARY_TAGS: ${{ needs.calculate-tags.outputs.primary-tags }} | |
| PYTHON_TAGS: ${{ needs.calculate-tags.outputs.python-tags }} | |
| run: | | |
| # Test primary tags can be pulled and work | |
| if [[ -n "$PRIMARY_TAGS" ]]; then | |
| IFS=',' read -ra TAGS <<< "$PRIMARY_TAGS" | |
| for tag in "${TAGS[@]}"; do | |
| echo "Testing primary tag: $tag" | |
| docker pull "$REGISTRY/$IMAGE:$tag" | |
| docker run --rm "$REGISTRY/$IMAGE:$tag" version | |
| echo "Primary tag $tag works" | |
| done | |
| fi | |
| # Test Python variant tags | |
| if [[ -n "$PYTHON_TAGS" ]]; then | |
| IFS=',' read -ra TAGS <<< "$PYTHON_TAGS" | |
| for tag in "${TAGS[@]}"; do | |
| echo "Testing Python variant tag: $tag" | |
| docker pull "$REGISTRY/$IMAGE:$tag" | |
| docker run --rm "$REGISTRY/$IMAGE:$tag" version | |
| echo "Python variant tag $tag works" | |
| done | |
| fi | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 | |
| - name: Test multi-architecture support | |
| env: | |
| REGISTRY: ${{ needs.config.outputs.container-registry }} | |
| IMAGE: ${{ needs.config.outputs.container-image }} | |
| VERSION: ${{ needs.config.outputs.package-version }} | |
| DEFAULT_PYTHON: ${{ needs.config.outputs.default-python-version }} | |
| run: | | |
| # Use buildx imagetools rather than `docker manifest inspect`: | |
| # imagetools authenticates via the existing docker login session and | |
| # handles both OCI and Docker manifest media types consistently for | |
| # GHCR-hosted images. | |
| REF="$REGISTRY/$IMAGE:$VERSION-python$DEFAULT_PYTHON" | |
| echo "Testing multi-arch manifest for $REF" | |
| INSPECT=$(docker buildx imagetools inspect --raw "$REF") | |
| echo "$INSPECT" | |
| if echo "$INSPECT" | grep -q '"architecture": "amd64"' && \ | |
| echo "$INSPECT" | grep -q '"architecture": "arm64"'; then | |
| echo "Multi-arch manifest contains both amd64 and arm64" | |
| else | |
| echo "Multi-arch manifest missing architectures" | |
| exit 1 | |
| fi | |