diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..5ef122d --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,206 @@ +# Builds and publishes the simrun container image to GHCR on version tags. +# +# Pipeline: +# 1. build - build each architecture on its own native runner, push by digest +# 2. merge - stitch the per-arch digests into one multiplatform manifest and +# tag it with the release version (e.g. 0.4.0, 0.4, latest) +# 3. sign - Cosign keyless (OIDC) signature + Syft SBOM attestation +# + +name: Publish Docker image + +on: + push: + tags: ["v*"] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + name: Build ${{ matrix.suffix }} + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-24.04 + suffix: amd64 + - platform: linux/arm64 + runner: ubuntu-24.04-arm + suffix: arm64 + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Set image name lowercase + run: echo "IMAGE_NAME_LC=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')" >> "${GITHUB_ENV}" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + with: + version: latest + + - name: Log in to GHCR + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }} + + - name: Build and push by digest + id: build + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + with: + context: . + platforms: ${{ matrix.platform }} + build-args: | + version=${{ steps.meta.outputs.version }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=simrun-${{ matrix.suffix }} + cache-to: type=gha,mode=max,scope=simrun-${{ matrix.suffix }} + # push-by-digest expects a single image manifest per arch; the default + # provenance attestation adds an extra manifest that breaks the merge. + provenance: false + outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }},push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: digest-${{ matrix.suffix }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + name: Create manifest + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + packages: write + outputs: + version: ${{ steps.meta.outputs.version }} + digest: ${{ steps.digest.outputs.digest }} + + steps: + - name: Set image name lowercase + run: echo "IMAGE_NAME_LC=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')" >> "${GITHUB_ENV}" + + - name: Download digests + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: /tmp/digests + pattern: digest-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + with: + version: latest + + - name: Log in to GHCR + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }} + # latest is applied automatically for semver tags (flavor latest=auto), + # so a workflow_dispatch from a branch will not move :latest. + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "${DOCKER_METADATA_OUTPUT_JSON}") \ + $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}@sha256:%s ' *) + + - name: Capture manifest digest + id: digest + run: | + tag=$(jq -r '.tags[0]' <<< "${DOCKER_METADATA_OUTPUT_JSON}") + digest=$(docker buildx imagetools inspect "${tag}" --format '{{json .Manifest.Digest}}' | tr -d '"') + echo "tag=${tag}" >> "${GITHUB_OUTPUT}" + echo "digest=${digest}" >> "${GITHUB_OUTPUT}" + + - name: Inspect manifest + run: docker buildx imagetools inspect "${{ steps.digest.outputs.tag }}" + + sign: + name: Sign and attest + needs: merge + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Set image name lowercase + run: echo "IMAGE_NAME_LC=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')" >> "${GITHUB_ENV}" + + - name: Install Cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + + - name: Log in to GHCR + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate SBOM (Syft) + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + with: + image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}@${{ needs.merge.outputs.digest }} + output-file: sbom.spdx.json + + - name: Sign and attest + env: + COSIGN_EXPERIMENTAL: "1" + run: | + REF="${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LC }}@${{ needs.merge.outputs.digest }}" + cosign sign --yes --recursive --registry-referrers-mode oci-1-1 "${REF}" + cosign attest --yes --recursive --registry-referrers-mode oci-1-1 \ + --predicate sbom.spdx.json --type spdxjson "${REF}" diff --git a/Dockerfile b/Dockerfile index 1e0a5d8..d017c33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1 # --- Build stage --- -FROM golang:1.25.10 AS builder +FROM golang:1.25.11 AS builder WORKDIR /build @@ -22,7 +22,8 @@ RUN rm -rf internal/web/frontend && \ # Build the server binary with the embedded frontend ARG version=unknown -RUN CGO_ENABLED=0 go build \ +RUN git config --global --add safe.directory /build && \ + CGO_ENABLED=0 go build \ -ldflags="-w -s \ -X github.com/IBM/simrun/internal/version.Version=${version} \ -X github.com/IBM/simrun/internal/version.Commit=$(git rev-parse --short HEAD 2>/dev/null || echo unknown) \ diff --git a/go.mod b/go.mod index b3b396c..0fa9f45 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/IBM/simrun -go 1.25.10 +go 1.25.11 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 diff --git a/mise.toml b/mise.toml index f45757a..5760ea6 100644 --- a/mise.toml +++ b/mise.toml @@ -1,5 +1,5 @@ [tools] -go = "1.25.10" +go = "1.25.11" golangci-lint = "2.12.2" mockery = "3.5.5" node = "22"