# Build and publish Docker images to GitHub Container Registry # # Triggers: # - On release: Build with version tags (X.Y.Z, X.Y, latest) # - On schedule: Rebuild all tags with fresh base image (OS patches) # - Manual: For testing, optional push # # Security: # - All actions pinned by SHA # - Vulnerability scanning with Trivy # - SBOM and provenance attestation name: Docker Build and Publish on: release: types: [published] schedule: # Daily at 4 AM UTC - rebuild with fresh base image - cron: "0 4 * * *" pull_request: paths: - Dockerfile - .dockerignore - docker/** - pyproject.toml - uv.lock - src/** - scripts/** - .github/workflows/docker-publish.yml workflow_dispatch: inputs: push: description: "Push image to registry" required: false default: false type: boolean permissions: contents: read packages: write id-token: write attestations: write artifact-metadata: write concurrency: group: docker-${{ github.ref }} cancel-in-progress: true env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build: if: github.event_name != 'pull_request' runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # For nightly builds, get the latest release version - name: Get latest release version id: get-version if: github.event_name == 'schedule' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Get latest release tag LATEST_TAG=$(gh release view --json tagName -q '.tagName' 2>/dev/null || echo "") if [ -z "$LATEST_TAG" ]; then echo "No releases found, skipping nightly build" echo "skip=true" >> $GITHUB_OUTPUT exit 0 fi # Strip 'v' prefix if present VERSION=$(echo "$LATEST_TAG" | sed 's/^v//') echo "version=$VERSION" >> $GITHUB_OUTPUT echo "skip=false" >> $GITHUB_OUTPUT - name: Skip if no releases if: github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true' run: | echo "No releases found, skipping nightly build" exit 0 - name: Set up QEMU if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')" uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Set up Docker Buildx if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')" uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Log in to Container Registry if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')" uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Generate tags based on event type - name: Extract metadata (release) id: meta-release if: github.event_name == 'release' uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | # X.Y.Z type=semver,pattern={{version}} # X.Y type=semver,pattern={{major}}.{{minor}} # latest type=raw,value=latest - name: Extract metadata (nightly) id: meta-nightly if: github.event_name == 'schedule' && steps.get-version.outputs.skip != 'true' uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | # Rebuild version tags with OS patches type=raw,value=${{ steps.get-version.outputs.version }} # Nightly tags type=raw,value=nightly type=raw,value=nightly-{{date 'YYYYMMDD'}} # Also update latest with security patches type=raw,value=latest - name: Extract metadata (manual) id: meta-manual if: github.event_name == 'workflow_dispatch' uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=sha,prefix=sha- # Build image (release - with cache) - name: Build and push (release) id: build-release if: github.event_name == 'release' uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta-release.outputs.tags }} labels: ${{ steps.meta-release.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max provenance: true sbom: true # Build image (nightly - no cache, fresh base) - name: Build and push (nightly) id: build-nightly if: github.event_name == 'schedule' && steps.get-version.outputs.skip != 'true' uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta-nightly.outputs.tags }} labels: ${{ steps.meta-nightly.outputs.labels }} pull: true no-cache: true provenance: true sbom: true # Build image (manual) - name: Build and push (manual) id: build-manual if: github.event_name == 'workflow_dispatch' uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . platforms: linux/amd64,linux/arm64 push: ${{ inputs.push }} tags: ${{ steps.meta-manual.outputs.tags }} labels: ${{ steps.meta-manual.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max provenance: true sbom: true # Determine image tag for scanning and testing - name: Determine image tag id: image-tag if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')" run: | if [ "${{ github.event_name }}" = "release" ]; then # Strip 'v' prefix to match semver tag format from metadata-action echo "tag=$(echo '${{ github.event.release.tag_name }}' | sed 's/^v//')" >> $GITHUB_OUTPUT elif [ "${{ github.event_name }}" = "schedule" ]; then echo "tag=nightly" >> $GITHUB_OUTPUT else echo "tag=sha-${{ github.sha }}" >> $GITHUB_OUTPUT fi # Vulnerability scanning - name: Run Trivy vulnerability scanner if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')" uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1 with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.image-tag.outputs.tag }} format: "sarif" output: "trivy-results.sarif" severity: "CRITICAL,HIGH" continue-on-error: true - name: Upload Trivy scan results if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true')" uses: github/codeql-action/upload-sarif@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 with: sarif_file: "trivy-results.sarif" continue-on-error: true # Smoke test - verify image runs correctly # Skip for manual runs when push is disabled (image not available to pull) - name: Smoke test if: "!(github.event_name == 'schedule' && steps.get-version.outputs.skip == 'true') && !(github.event_name == 'workflow_dispatch' && inputs.push == false)" run: | IMAGE_TAG="${{ steps.image-tag.outputs.tag }}" docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG} # Test that Python and key modules are available docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG} \ python -c "from meshmon.db import init_db; from meshmon.env import get_config; print('Smoke test passed')" # Attestation (releases only) - name: Generate attestation if: github.event_name == 'release' uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.build-release.outputs.digest }} push-to-registry: true build-pr: if: github.event_name == 'pull_request' runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Build image (PR) id: build-pr uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . platforms: linux/amd64 load: true push: false tags: meshcore-stats:pr-${{ github.event.pull_request.number }} cache-from: type=gha cache-to: type=gha,mode=max - name: Smoke test (PR) run: | docker run --rm meshcore-stats:pr-${{ github.event.pull_request.number }} \ python -c "from meshmon.db import init_db; from meshmon.env import get_config; print('Smoke test passed')"