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: {} env: IMAGE_DESCRIPTION: Campfire is a web-based chat application with multiple rooms, direct messages, file attachments with previews, search, web push notifications, @mentions, and bot integrations. Single-tenant; production-ready image with web app, background jobs, caching, file serving, and SSL. SOURCE_URL: https://github.com/${{ github.repository }} jobs: build: name: Build and push image (${{ matrix.arch }}) runs-on: ${{ matrix.runner }} permissions: contents: read packages: write id-token: write attestations: write 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Log in to GHCR uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.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 app version id: version shell: bash run: | set -eu if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" else echo "version=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" fi - name: Extract Docker metadata (tags, labels) with arch suffix id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . file: Dockerfile build-args: | OCI_SOURCE=${{ env.SOURCE_URL }} OCI_DESCRIPTION=${{ env.IMAGE_DESCRIPTION }} APP_VERSION=${{ steps.version.outputs.version }} GIT_REVISION=${{ github.sha }} 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@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 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 permissions: contents: read packages: write id-token: write timeout-minutes: 20 env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} steps: - name: Set up Docker Buildx (for imagetools) uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Log in to GHCR uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.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@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.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 env: TAGS: ${{ steps.meta.outputs.tags }} run: | set -eu tags="$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@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Cosign sign all tags (keyless OIDC) shell: bash env: TAGS: ${{ steps.meta.outputs.tags }} run: | set -eu tags="$TAGS" printf '%s\n' "$tags" while IFS= read -r tag; do [ -z "$tag" ] && continue echo "Signing $tag" cosign sign --yes "$tag" done <<< "$tags"