diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml new file mode 100644 index 000000000..0c02245c2 --- /dev/null +++ b/.github/workflows/publish-image.yml @@ -0,0 +1,202 @@ +name: Build and publish container image to GHCR + +on: + push: + branches: + - main + tags: + - 'v*' + pull_request: + workflow_dispatch: + +concurrency: + group: publish-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + id-token: write + attestations: write + +env: + IMAGE_DESCRIPTION: Fizzy is Kanban as it should be. Not as it has been. + SOURCE_URL: https://github.com/${{ github.repository }} + +jobs: + build: + name: Build and push image (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + platform: linux/amd64 + arch: amd64 + - runner: ubuntu-24.04-arm + platform: linux/arm64 + arch: arm64 + env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + steps: + - name: Checkout + uses: actions/checkout@v5.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.11.1 + + - name: Log in to GHCR + uses: docker/login-action@v3.5.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute canonical image name (lowercase) + id: vars + shell: bash + run: | + set -eu + IMAGE_REF="${IMAGE_NAME:-$GITHUB_REPOSITORY}" + CANONICAL_IMAGE="${REGISTRY}/${IMAGE_REF,,}" + echo "canonical=${CANONICAL_IMAGE}" >> "$GITHUB_OUTPUT" + + - name: Extract Docker metadata (tags, labels) with arch suffix + id: meta + uses: docker/metadata-action@v5.8.0 + with: + images: ${{ steps.vars.outputs.canonical }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,format=short,prefix=sha- + type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + flavor: | + latest=false + suffix=-${{ matrix.arch }} + labels: | + org.opencontainers.image.source=${{ env.SOURCE_URL }} + + - name: Build and push (${{ matrix.platform }}) + id: build + uses: docker/build-push-action@v6.18.0 + with: + context: . + file: Dockerfile + build-args: | + OCI_SOURCE=${{ env.SOURCE_URL }} + OCI_DESCRIPTION=${{ env.IMAGE_DESCRIPTION }} + platforms: ${{ matrix.platform }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.platform }} + cache-to: type=gha,scope=${{ matrix.platform }},mode=max + sbom: false + provenance: false + + - name: Attest image provenance (per-arch) + if: github.event_name != 'pull_request' + uses: actions/attest-build-provenance@v3.0.0 + with: + subject-name: ${{ steps.vars.outputs.canonical }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: false + + manifest: + name: Create multi-arch manifest and sign + needs: build + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + steps: + - name: Set up Docker Buildx (for imagetools) + uses: docker/setup-buildx-action@v3.11.1 + + - name: Log in to GHCR + uses: docker/login-action@v3.5.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute canonical image name (lowercase) + id: vars + shell: bash + run: | + set -eu + IMAGE_REF="${IMAGE_NAME:-$GITHUB_REPOSITORY}" + CANONICAL_IMAGE="${REGISTRY}/${IMAGE_REF,,}" + echo "canonical=${CANONICAL_IMAGE}" >> "$GITHUB_OUTPUT" + + - name: Compute base tags (no suffix) + id: meta + uses: docker/metadata-action@v5.8.0 + with: + images: ${{ steps.vars.outputs.canonical }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,format=short,prefix=sha- + type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + flavor: | + latest=false + labels: | + org.opencontainers.image.source=${{ env.SOURCE_URL }} + + - name: Create multi-arch manifests + shell: bash + run: | + set -eu + tags="${{ steps.meta.outputs.tags }}" + echo "Creating manifests for tags:" + printf '%s\n' "$tags" + while IFS= read -r tag; do + [ -z "$tag" ] && continue + echo "Creating manifest for $tag" + src_tag="$tag" + if [[ "$tag" == *:latest && "${GITHUB_REF}" == refs/tags/* ]]; then + ref="${GITHUB_REF#refs/tags/}" + src_tag="${tag%:latest}:$ref" + fi + if [ -n "${IMAGE_DESCRIPTION:-}" ]; then + docker buildx imagetools create \ + --tag "$tag" \ + --annotation "index:org.opencontainers.image.description=${IMAGE_DESCRIPTION}" \ + "${src_tag}-amd64" \ + "${src_tag}-arm64" + else + docker buildx imagetools create \ + --tag "$tag" \ + "${src_tag}-amd64" \ + "${src_tag}-arm64" + fi + done <<< "$tags" + + - name: Install Cosign + uses: sigstore/cosign-installer@v3.9.2 + + - name: Cosign sign all tags (keyless OIDC) + shell: bash + run: | + set -eu + tags="${{ steps.meta.outputs.tags }}" + printf '%s\n' "$tags" + while IFS= read -r tag; do + [ -z "$tag" ] && continue + echo "Signing $tag" + cosign sign --yes "$tag" + done <<< "$tags" + + diff --git a/Dockerfile b/Dockerfile index 3cd25c7f1..6033d297e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,6 +59,13 @@ RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile # Final stage for app image FROM base +# Image metadata +ARG OCI_DESCRIPTION +LABEL org.opencontainers.image.description="${OCI_DESCRIPTION}" +ARG OCI_SOURCE +LABEL org.opencontainers.image.source="${OCI_SOURCE}" +LABEL org.opencontainers.image.licenses="O'Saasy" + # Run and own only the runtime files as a non-root user for security RUN groupadd --system --gid 1000 rails && \ useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash