diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..f03393d --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,183 @@ +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: read + 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@v3 + 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' }} diff --git a/Dockerfile b/Dockerfile index 9d42135..315b242 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,7 +49,9 @@ COPY frontend/package.json frontend/vite.config.js ./frontend/ RUN cd frontend && npm install COPY frontend/ ./frontend/ -RUN cd frontend && npm run build +# ARG is declared here so it only busts the cache for this layer +ARG VITE_APP_VERSION=dev +RUN cd frontend && VITE_APP_VERSION=${VITE_APP_VERSION} npm run build # Copy everything else COPY backend/ ./backend/ diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 5cf8630..6f489e8 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -19,7 +19,7 @@ export default function Header({ clusterReady }) {