name: Build and Push Docker Images on: push: branches: - main - develop tags: - 'v*' pull_request: branches: - main - develop pull_request_target: types: [labeled] workflow_dispatch: env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: # Security check for fork PRs security-check: runs-on: ubuntu-latest if: github.event_name == 'pull_request' permissions: pull-requests: read outputs: is_fork: ${{ steps.check.outputs.is_fork }} steps: - name: Check if PR is from fork id: check run: | if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then echo "is_fork=true" >> $GITHUB_OUTPUT echo "::warning::This PR is from a fork. Builds from forks require manual approval for security." else echo "is_fork=false" >> $GITHUB_OUTPUT fi build-and-push: needs: security-check # Run on push/tag events (security-check is skipped but that's ok) # For PRs, only run on non-fork PRs or manually approved fork PRs if: | always() && ( github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && needs.security-check.result == 'success' && needs.security-check.outputs.is_fork == 'false') || (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-build')) ) runs-on: ubuntu-latest permissions: contents: read packages: write strategy: matrix: include: - service: web dockerfile: docker/web/Dockerfile context: . - service: caddy dockerfile: docker/caddy/Dockerfile context: . steps: - name: Checkout repository uses: actions/checkout@v5 with: # For pull_request_target, checkout the PR head ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-${{ matrix.service }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=sha type=raw,value=latest,enable={{is_default_branch}} - name: Build and push Docker image id: build uses: docker/build-push-action@v6 with: context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} push: ${{ github.event_name != 'pull_request' && github.event_name != 'pull_request_target' }} load: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max # Build amd64 images on pushes; PR builds stay single-platform platforms: ${{ (github.event_name != 'pull_request' && github.event_name != 'pull_request_target') && 'linux/amd64' || '' }} # SBOM and provenance create manifest lists, incompatible with load (PRs) sbom: ${{ github.event_name != 'pull_request' && github.event_name != 'pull_request_target' }} provenance: ${{ github.event_name != 'pull_request' && github.event_name != 'pull_request_target' }}