name: Release to Docker Hub on: push: tags: - "v*.*.*" # stable - "v*.*.*-*" # pre-release env: IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} IS_PRERELEASE: ${{ contains(github.ref_name, '-') }} jobs: build-and-push: name: Build & Push Docker Image runs-on: ubuntu-latest permissions: contents: write id-token: write attestations: write outputs: digest: ${{ steps.push.outputs.digest }} steps: - name: Checkout repository uses: actions/checkout@v4 # Extract metadata (tags, labels) for Docker - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.IMAGE_NAME }} flavor: | latest=${{ env.IS_PRERELEASE == 'false' }} prefix=v,onlatest=false tags: | type=semver,pattern={{version}},enable=${{ env.IS_PRERELEASE == 'false' }} type=semver,pattern={{major}}.{{minor}},enable=${{ env.IS_PRERELEASE == 'false' }} type=semver,pattern={{major}},enable=${{ env.IS_PRERELEASE == 'false' }} type=semver,pattern={{version}},enable=${{ env.IS_PRERELEASE == 'true' }} type=raw,value=${{ contains(github.ref_name, '-rc') && 'rc' || contains(github.ref_name, '-beta') && 'beta' || contains(github.ref_name, '-alpha') && 'alpha' || 'pre' }},enable=${{ env.IS_PRERELEASE == 'true' }} type=raw,value=edge,enable=${{ env.IS_PRERELEASE == 'true' }} labels: | org.opencontainers.image.title=${{ github.event.repository.name }} org.opencontainers.image.description=${{ github.event.repository.description }} org.opencontainers.image.url=${{ github.event.repository.html_url }} org.opencontainers.image.source=${{ github.event.repository.clone_url }} org.opencontainers.image.revision=${{ github.sha }} # Log in to Docker Hub - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} # Set up QEMU for multi-platform builds - name: Set up QEMU uses: docker/setup-qemu-action@v3 # Set up Docker Buildx for multi-platform support - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 # Build and push Docker image - name: Build and push Docker image id: push uses: docker/build-push-action@v6 with: context: . push: true platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | VITE_APP_VERSION=${{ github.ref_name }} # Generate SBOMs for each platform and attach to the image sbom: true # Also generate provenance attestation via buildkit provenance: mode=max cache-from: type=gha cache-to: type=gha,mode=max # Attest build provenance via GitHub's attestation store - name: Attest build provenance uses: actions/attest-build-provenance@v1 with: subject-name: docker.io/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.push.outputs.digest }} push-to-registry: true # Generate an SBOM with Anchore Syft and attach it as a GitHub attestation - name: Generate SBOM (Syft) uses: anchore/sbom-action@v0 id: sbom with: image: docker.io/${{ env.IMAGE_NAME }}@${{ steps.push.outputs.digest }} format: spdx-json output-file: sbom.spdx.json upload-artifact: true upload-artifact-retention: 90 # Attest the SBOM so consumers can verify it was produced by this workflow - name: Attest SBOM uses: actions/attest-sbom@v1 with: subject-name: docker.io/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.push.outputs.digest }} sbom-path: sbom.spdx.json push-to-registry: true create-github-release: name: Create GitHub Release runs-on: ubuntu-latest needs: build-and-push permissions: contents: write steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 # Retrieve the SBOM artifact produced in the previous job - name: Download SBOM artifact uses: actions/download-artifact@v4 with: name: ${{ github.event.repository.name }}-${{ github.ref_name }}.spdx.json path: release-assets/ continue-on-error: true # Auto-generate a changelog from commits since the last tag - name: Generate changelog id: changelog uses: orhun/git-cliff-action@v4 with: args: --latest --strip header continue-on-error: true - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} name: ${{ env.IS_PRERELEASE == 'true' && format('Pre-release {0}', github.ref_name) || format('Release {0}', github.ref_name) }} body: | ${{ env.IS_PRERELEASE == 'true' && '> ⚠️ **This is a pre-release.** It may be unstable. Do not use in production without testing.' || '' }} ## Docker image ``` docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:${{ github.ref_name }} ``` ${{ env.IS_PRERELEASE == 'true' && format('You can also pull the rolling pre-release channel tag:\n```\ndocker pull {0}/{1}:{2}\n```', secrets.DOCKERHUB_USERNAME, github.event.repository.name, contains(github.ref_name, '-rc') && 'rc' || contains(github.ref_name, '-beta') && 'beta' || contains(github.ref_name, '-alpha') && 'alpha' || 'edge') || '' }} ## Supply chain security This release includes: - **Build provenance attestation** — verifiable record of how and where the image was built - **SBOM** (Software Bill of Materials) — full inventory of packages inside the image Verify with the GitHub CLI: ``` gh attestation verify oci://docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:${{ github.ref_name }} \ --repo ${{ github.repository }} ``` Or scan for vulnerabilities using [Grype](https://github.com/anchore/grype): ``` grype ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:${{ github.ref_name }} ``` ${{ steps.changelog.outputs.content }} files: release-assets/** draft: false prerelease: ${{ env.IS_PRERELEASE == 'true' }} make_latest: ${{ env.IS_PRERELEASE == 'false' }}