Re-sync frontend UI primitives with the @nebari registry #1225
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and Push Docker Image | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - "main" | |
| concurrency: | |
| group: ${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| # vars.* are not exposed to fork-PR runs on public repos, so fallbacks | |
| # keep the image tag valid for the local build + smoke test. | |
| REGISTRY: ${{ vars.DOCKER_REGISTRY || 'localhost' }} | |
| PROJECT: ${{ vars.DOCKER_PROJECT || 'fork-pr' }} | |
| IMAGE_NAME: nebi | |
| jobs: | |
| build: | |
| name: Build (${{ matrix.platform }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Each platform builds, smoke-tests, and scans natively on its own | |
| # runner (#417): no QEMU emulation, no cross-compilation. | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| fetch-depth: 0 | |
| # linux/amd64 -> linux-amd64, used in cache scopes and artifact names | |
| - name: Prepare platform slug | |
| env: | |
| PLATFORM: ${{ matrix.platform }} | |
| run: echo "PLATFORM_PAIR=${PLATFORM//\//-}" >> "$GITHUB_ENV" | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Compute version | |
| id: version | |
| # git describe matches the format internal/api/handlers/version.go parses | |
| # (e.g. v0.12-rc3-4-ge8f6759 -> 0.12-rc3.dev+e8f6759). fetch-depth: 0 above | |
| # makes tags available. Without this the image bakes VERSION=dev and the | |
| # UI shows "vdev". | |
| run: echo "version=$(git describe --tags --always --dirty)" >> "$GITHUB_OUTPUT" | |
| - name: Docker metadata | |
| id: meta | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 | |
| env: | |
| DOCKER_METADATA_PR_HEAD_SHA: true | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.PROJECT }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| # PR builds: tag with pr-<number> and sha only | |
| type=ref,event=pr | |
| type=sha | |
| # Main branch builds: add branch name and latest | |
| type=ref,event=branch,enable={{is_default_branch}} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| # Release builds | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| - name: Determine registry publishing | |
| id: registry | |
| shell: bash | |
| env: | |
| DOCKER_REGISTRY_USERNAME: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | |
| DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} | |
| run: | | |
| PUBLISH=false | |
| if [[ '${{ github.event_name }}' != 'pull_request' ]]; then | |
| PUBLISH=true | |
| elif [[ -n "${DOCKER_REGISTRY_USERNAME}" && -n "${DOCKER_REGISTRY_PASSWORD}" ]]; then | |
| PUBLISH=true | |
| fi | |
| echo "publish=${PUBLISH}" | tee --append "${GITHUB_OUTPUT}" | |
| - name: Login to Quay.io | |
| if: steps.registry.outputs.publish == 'true' | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | |
| password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} | |
| # Build locally only; the digest push happens in a later step so the | |
| # smoke test and vulnerability scan gate what gets published. | |
| - name: Build image | |
| id: build | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| build-args: | | |
| VERSION=${{ steps.version.outputs.version }} | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| platforms: ${{ matrix.platform }} | |
| push: false | |
| load: true | |
| cache-from: ${{ steps.registry.outputs.publish == 'true' && format('type=gha,scope={0}', env.PLATFORM_PAIR) || '' }} | |
| cache-to: ${{ steps.registry.outputs.publish == 'true' && format('type=gha,mode=max,scope={0}', env.PLATFORM_PAIR) || '' }} | |
| - name: Smoke test | |
| run: | | |
| IMAGE_TAG="${{ env.REGISTRY }}/${{ env.PROJECT }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}" | |
| echo "Testing image: ${IMAGE_TAG}" | |
| # Start container with port mapping | |
| docker run --rm -d --name test-nebi \ | |
| -p 8460:8460 \ | |
| -e NEBI_AUTH_JWT_SECRET=test-secret-that-is-at-least-32-chars-long \ | |
| -e NEBI_DATABASE_DSN=/tmp/test.db \ | |
| ${IMAGE_TAG} | |
| # Wait for startup | |
| sleep 10 | |
| # Check if container is still running | |
| docker ps | grep test-nebi | |
| # Check logs | |
| docker logs test-nebi | |
| # Test health endpoint from host (pixi image has no wget/curl) | |
| curl -sf http://localhost:8460/api/v1/health || exit 1 | |
| # Guard: the image must report a real version, not the "dev" default. | |
| # Catches a dropped VERSION build-arg (the bug that made the UI show "vdev"). | |
| VERSION_JSON=$(curl -sf http://localhost:8460/api/v1/version) | |
| echo "Version endpoint: ${VERSION_JSON}" | |
| REPORTED=$(echo "${VERSION_JSON}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["version"])') | |
| echo "Reported version: ${REPORTED}" | |
| if [ "${REPORTED}" = "dev" ] || [ -z "${REPORTED}" ]; then | |
| echo "ERROR: image reports version '${REPORTED}' — VERSION build-arg was not injected" | |
| exit 1 | |
| fi | |
| # Cleanup | |
| docker stop test-nebi | |
| # Scan the locally built image (base layers + compiled binary) and fail | |
| # on fixable High/Critical findings (#451). Runs before the push step so | |
| # an image that fails the scan is never published. | |
| - name: Scan image for vulnerabilities | |
| uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 | |
| with: | |
| image-ref: ${{ env.REGISTRY }}/${{ env.PROJECT }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} | |
| format: table | |
| exit-code: '1' | |
| severity: CRITICAL,HIGH | |
| ignore-unfixed: true | |
| # Push the image that passed the smoke test and scan, addressed by | |
| # digest only (no tags yet). Rebuilds from this runner's BuildKit cache, | |
| # so the pushed layers are the ones that were tested. The merge job | |
| # stitches the per-platform digests into one tagged manifest list. | |
| - name: Push image by digest | |
| id: push | |
| if: steps.registry.outputs.publish == 'true' | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| build-args: | | |
| VERSION=${{ steps.version.outputs.version }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| platforms: ${{ matrix.platform }} | |
| outputs: type=image,name=${{ env.REGISTRY }}/${{ env.PROJECT }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | |
| cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }} | |
| - name: Export digest | |
| if: steps.registry.outputs.publish == 'true' | |
| run: | | |
| mkdir -p "${{ runner.temp }}/digests" | |
| digest='${{ steps.push.outputs.digest }}' | |
| touch "${{ runner.temp }}/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| if: steps.registry.outputs.publish == 'true' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: digests-${{ env.PLATFORM_PAIR }} | |
| path: ${{ runner.temp }}/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # Combine the per-platform digests pushed above into a single manifest | |
| # list carrying the real tags. Runs only if every platform passed its | |
| # smoke test and scan. | |
| merge: | |
| name: Push manifest list | |
| runs-on: ubuntu-latest | |
| needs: build | |
| steps: | |
| - name: Determine registry publishing | |
| id: registry | |
| shell: bash | |
| env: | |
| DOCKER_REGISTRY_USERNAME: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | |
| DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} | |
| run: | | |
| PUBLISH=false | |
| if [[ '${{ github.event_name }}' != 'pull_request' ]]; then | |
| PUBLISH=true | |
| elif [[ -n "${DOCKER_REGISTRY_USERNAME}" && -n "${DOCKER_REGISTRY_PASSWORD}" ]]; then | |
| PUBLISH=true | |
| fi | |
| echo "publish=${PUBLISH}" | tee --append "${GITHUB_OUTPUT}" | |
| - name: Docker metadata | |
| id: meta | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 | |
| env: | |
| DOCKER_METADATA_PR_HEAD_SHA: true | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.PROJECT }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| # PR builds: tag with pr-<number> and sha only | |
| type=ref,event=pr | |
| type=sha | |
| # Main branch builds: add branch name and latest | |
| type=ref,event=branch,enable={{is_default_branch}} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| # Release builds | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| - name: Download digests | |
| if: steps.registry.outputs.publish == 'true' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: ${{ runner.temp }}/digests | |
| pattern: digests-* | |
| merge-multiple: true | |
| - name: Set up Docker Buildx | |
| if: steps.registry.outputs.publish == 'true' | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Login to Quay.io | |
| if: steps.registry.outputs.publish == 'true' | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | |
| password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} | |
| - name: Create and push manifest list | |
| if: steps.registry.outputs.publish == 'true' | |
| working-directory: ${{ runner.temp }}/digests | |
| env: | |
| TAGS: ${{ steps.meta.outputs.tags }} | |
| IMAGE: ${{ env.REGISTRY }}/${{ env.PROJECT }}/${{ env.IMAGE_NAME }} | |
| run: | | |
| TAG_ARGS=() | |
| while IFS= read -r tag; do | |
| [ -z "${tag}" ] && continue | |
| TAG_ARGS+=(-t "${tag}") | |
| done <<< "${TAGS}" | |
| REFS=() | |
| for digest in *; do | |
| REFS+=("${IMAGE}@sha256:${digest}") | |
| done | |
| docker buildx imagetools create "${TAG_ARGS[@]}" "${REFS[@]}" | |
| - name: Inspect manifest list | |
| if: steps.registry.outputs.publish == 'true' | |
| run: | | |
| docker buildx imagetools inspect \ | |
| '${{ env.REGISTRY }}/${{ env.PROJECT }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}' | |
| - name: Add image tags to summary | |
| run: | | |
| echo "## Docker Image Built: ${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [[ '${{ steps.registry.outputs.publish }}' == 'true' ]]; then | |
| echo "**Platforms:** linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "**Platforms:** linux/amd64, linux/arm64 (local build only, not published)" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Pull Command:" >> $GITHUB_STEP_SUMMARY | |
| echo '```bash' >> $GITHUB_STEP_SUMMARY | |
| echo "docker pull ${{ env.REGISTRY }}/${{ env.PROJECT }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### All Tags:" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| echo "${{ steps.meta.outputs.tags }}" | sed 's/,/\n/g' >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY |