name: Build and Push Docker Image on: push: branches: [main] tags: ['v*.*.*'] env: REGISTRY: ghcr.io IMAGE_NAME: erp-mafia/gnubok jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write # OIDC token used by cosign keyless signing. id-token: write # SARIF upload to the repo's "Security" tab from the Trivy scan. security-events: write steps: - uses: actions/checkout@v7 - name: Log in to GHCR uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} # On main: push `latest` + commit-sha tags. # On v*.*.* tags: push semver tags (1.2.3, 1.2, 1) for production pinning. tags: | type=raw,value=latest,enable={{is_default_branch}} type=sha,prefix= type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - name: Set up QEMU uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Build and push id: build uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | EXTENSIONS_PRESET=self-hosted # SBOM (software bill of materials) + SLSA provenance are attached as # OCI attestations, queryable via `docker buildx imagetools inspect`. provenance: mode=max sbom: true # Per-branch cache scope so a PR branch can't poison main's cache # layers. Fall back to main's cache on first build of a new branch. cache-from: | type=gha,scope=${{ github.ref_name }} type=gha,scope=main cache-to: type=gha,scope=${{ github.ref_name }},mode=max - name: Install cosign uses: sigstore/cosign-installer@v3.7.0 - name: Sign the image (keyless OIDC) env: DIGEST: ${{ steps.build.outputs.digest }} run: | cosign sign --yes "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" - name: Scan image with Trivy (report-only) id: trivy # Decoupled from the publish gate on purpose: this pipeline must stay # green so builds are deterministic. The image is already pushed and # signed above, so failing here would only redden the run: it would not # unship a vulnerable image. exit-code:0 + continue-on-error keep CVEs # (and even a Trivy/DB outage) from failing the build; findings still # flow to the Security tab below. The real blocking gate is # docker-image-scan.yml, which re-scans the published image and fails # (notifying admins) on a fixable CRITICAL/HIGH CVE. It runs on a # workflow_run trigger the moment THIS workflow completes (so the gap # between publish and the blocking scan is the scan's own duration # (minutes), not a 24h cron window) plus a daily cron as a safety net. # Accepted residual risk: an image is live for that short scan window # before the gate fires; see docs/SELF-HOSTING.md / the risk register. continue-on-error: true uses: aquasecurity/trivy-action@v0.36.0 with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} severity: CRITICAL,HIGH exit-code: '0' ignore-unfixed: true format: sarif output: trivy-results.sarif - name: Upload Trivy results to GitHub Security tab # if: always(), evidence must reach the Security tab regardless of the # scan step's exit status. With the previous `outcome == 'success'` guard, # a Trivy/DB outage that errored the scan would silently drop findings. # Kept non-fatal (continue-on-error) so a missing SARIF or a Security-tab # hiccup can't redden an otherwise-good publish. if: always() continue-on-error: true uses: github/codeql-action/upload-sarif@v3 with: sarif_file: trivy-results.sarif category: trivy