diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 3d4cf325..50f310cb 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -24,7 +24,7 @@ jobs: issues: write id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 0e5365ee..53ef9c0d 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -10,6 +10,11 @@ on: description: 'Release version (e.g., 25.11.1)' required: true type: string + test_mode: + description: 'Test mode: push version/short tags only, NOT latest (use for CI verification so a test run cannot clobber the real latest tag)' + required: false + type: boolean + default: false permissions: contents: write @@ -17,6 +22,11 @@ permissions: env: REGISTRY_GHCR: ghcr.io IMAGE_NAME: basekick-labs/arc + # Docker Hub mirror — published alongside GHCR on every release. + # DOCKERHUB_USERNAME / DOCKERHUB_TOKEN are repo secrets (the token is a + # Docker Hub access token, not the account password). + REGISTRY_DOCKERHUB: docker.io + IMAGE_NAME_DOCKERHUB: basekicklabs/arc jobs: # Extract version from branch name or input @@ -26,6 +36,7 @@ jobs: outputs: version: ${{ steps.version.outputs.version }} short_version: ${{ steps.version.outputs.short_version }} + test_mode: ${{ steps.version.outputs.test_mode }} steps: - name: Extract version id: version @@ -46,9 +57,19 @@ jobs: # Extract short version (25.11) SHORT_VERSION="${VERSION%.*}" + # test_mode (workflow_dispatch only) suppresses the `latest` + # tag so a CI verification run cannot overwrite the real + # latest on either registry. Branch-push releases are never + # test mode. + TEST_MODE="${{ inputs.test_mode }}" + if [ "${{ github.event_name }}" != "workflow_dispatch" ]; then + TEST_MODE="false" + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT echo "short_version=$SHORT_VERSION" >> $GITHUB_OUTPUT - echo "📦 Building version: $VERSION" >> $GITHUB_STEP_SUMMARY + echo "test_mode=$TEST_MODE" >> $GITHUB_OUTPUT + echo "📦 Building version: $VERSION (test_mode=$TEST_MODE)" >> $GITHUB_STEP_SUMMARY # Build and push Docker multi-arch images docker-build: @@ -65,16 +86,16 @@ jobs: - linux/arm64 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY_GHCR }} username: ${{ github.actor }} @@ -82,18 +103,24 @@ jobs: - name: Extract metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | ${{ env.REGISTRY_GHCR }}/${{ env.IMAGE_NAME }} tags: | type=raw,value=${{ needs.prepare.outputs.version }} type=raw,value=${{ needs.prepare.outputs.short_version }} - type=raw,value=latest - - - name: Build and push by digest - id: build - uses: docker/build-push-action@v5 + type=raw,value=latest,enable=${{ needs.prepare.outputs.test_mode != 'true' }} + + # GHCR uses the digest-and-merge pattern: each platform pushes its + # image by digest, and docker-merge combines them into a multi-arch + # manifest. GHCR's package UI indexes these registry-API pushes + # correctly. (Docker Hub does NOT — its Tags page only indexes the + # standard buildx --push path — so Docker Hub is handled separately + # in the docker-build-dockerhub job below, not here.) + - name: Build and push by digest (GHCR) + id: build_ghcr + uses: docker/build-push-action@v7 with: context: . platforms: ${{ matrix.platform }} @@ -107,17 +134,77 @@ jobs: - name: Export digest run: | mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" + digest="${{ steps.build_ghcr.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: digests-${{ strategy.job-index }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 + # Docker Hub gets a single multi-platform `buildx --push` (not the + # digest-and-merge pattern). imagetools/digest-merge pushes via the + # registry manifest API, which Docker Hub's Tags page does NOT index — + # the image is pullable but the Tags listing stays empty. A direct + # multi-platform build-push goes through the path Hub's UI indexes, so + # the published tags actually show up. Layers come from the same gha + # cache the GHCR build populated, so this is mostly cache-hit + push. + docker-build-dockerhub: + name: Build & Push Docker Hub + runs-on: ubuntu-latest + needs: prepare + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY_DOCKERHUB }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: | + ${{ env.REGISTRY_DOCKERHUB }}/${{ env.IMAGE_NAME_DOCKERHUB }} + tags: | + type=raw,value=${{ needs.prepare.outputs.version }} + type=raw,value=${{ needs.prepare.outputs.short_version }} + type=raw,value=latest,enable=${{ needs.prepare.outputs.test_mode != 'true' }} + + - name: Build and push (multi-platform, direct) + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ needs.prepare.outputs.version }} + + - name: Summary + run: | + echo "✅ Docker Hub multi-arch image pushed via direct buildx --push" >> $GITHUB_STEP_SUMMARY + echo "Tags:" >> $GITHUB_STEP_SUMMARY + echo '${{ steps.meta.outputs.tags }}' | sed 's/^/ - /' >> $GITHUB_STEP_SUMMARY + # Build Go binaries using native runners (CGO required for SQLite) build-binaries: name: Build Binaries @@ -132,10 +219,10 @@ jobs: runs-on: ${{ matrix.runner }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: '1.26' cache: true @@ -158,7 +245,7 @@ jobs: ls -lh arc-${{ matrix.suffix }} - name: Upload binary - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: arc-binary-${{ matrix.suffix }} path: arc-${{ matrix.suffix }} @@ -178,10 +265,10 @@ jobs: binary_suffix: linux-arm64 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Download binary - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: arc-binary-${{ matrix.binary_suffix }} @@ -290,7 +377,7 @@ jobs: sha256sum ${PKG_DIR}.deb > ${PKG_DIR}.deb.sha256 - name: Upload Debian package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: arc-debian-${{ matrix.arch }}-${{ needs.prepare.outputs.version }} path: | @@ -312,7 +399,7 @@ jobs: binary_suffix: linux-arm64 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install RPM tools run: | @@ -320,7 +407,7 @@ jobs: sudo apt-get install -y rpm - name: Download binary - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: arc-binary-${{ matrix.binary_suffix }} @@ -431,7 +518,7 @@ jobs: sha256sum arc-${VERSION}-1.*.rpm > arc-${VERSION}-1.${ARCH}.rpm.sha256 - name: Upload RPM package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: arc-rpm-${{ matrix.arch }}-${{ needs.prepare.outputs.version }} path: | @@ -449,34 +536,46 @@ jobs: packages: write steps: - name: Download digests - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: pattern: digests-* merge-multiple: true path: /tmp/digests - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY_GHCR }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Create manifest list and push + - name: Create manifest list and push (GHCR) working-directory: /tmp/digests + env: + IMAGE_REF: ${{ env.REGISTRY_GHCR }}/${{ env.IMAGE_NAME }} + VERSION: ${{ needs.prepare.outputs.version }} + SHORT_VERSION: ${{ needs.prepare.outputs.short_version }} + TEST_MODE: ${{ needs.prepare.outputs.test_mode }} run: | - # Create multi-arch manifest with version, short_version, and latest tags + # Multi-arch manifest with version + short_version, plus latest + # unless this is a test_mode run (which must not clobber latest). + # Docker Hub is published separately (docker-build-dockerhub) via + # a direct buildx --push so its Tags UI indexes the release. + TAGS=( -t "${IMAGE_REF}:${VERSION}" -t "${IMAGE_REF}:${SHORT_VERSION}" ) + if [ "${TEST_MODE}" != "true" ]; then + TAGS+=( -t "${IMAGE_REF}:latest" ) + fi docker buildx imagetools create \ - -t ${{ env.REGISTRY_GHCR }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.version }} \ - -t ${{ env.REGISTRY_GHCR }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.short_version }} \ - -t ${{ env.REGISTRY_GHCR }}/${{ env.IMAGE_NAME }}:latest \ - $(printf '${{ env.REGISTRY_GHCR }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) + "${TAGS[@]}" \ + $(printf "${IMAGE_REF}@sha256:%s " *) - echo "✅ Multi-arch manifest created and pushed" >> $GITHUB_STEP_SUMMARY - echo "Tags: ${{ needs.prepare.outputs.version }}, ${{ needs.prepare.outputs.short_version }}, latest" >> $GITHUB_STEP_SUMMARY + LATEST_NOTE="latest" + [ "${TEST_MODE}" = "true" ] && LATEST_NOTE="(latest suppressed — test_mode)" + echo "✅ GHCR multi-arch manifest pushed" >> $GITHUB_STEP_SUMMARY + echo "Tags: ${VERSION}, ${SHORT_VERSION}, ${LATEST_NOTE}" >> $GITHUB_STEP_SUMMARY # Test Docker image health test-docker: @@ -542,7 +641,7 @@ jobs: needs: [prepare, build-binaries] steps: - name: Download amd64 binary - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: arc-binary-linux-amd64 @@ -584,10 +683,10 @@ jobs: needs: prepare steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install Helm - uses: azure/setup-helm@v4 + uses: azure/setup-helm@v5 with: version: '3.13.0' @@ -607,7 +706,7 @@ jobs: sha256sum arc-${VERSION}.tgz > arc-${VERSION}.tgz.sha256 - name: Upload Helm chart - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: arc-helm-${{ needs.prepare.outputs.version }} path: | @@ -622,19 +721,19 @@ jobs: needs: [prepare, helm-package] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Download Helm chart - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: arc-helm-${{ needs.prepare.outputs.version }} path: dist/ - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build Docker image for testing - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . push: false @@ -644,10 +743,10 @@ jobs: cache-to: type=gha,mode=max - name: Install kubectl - uses: azure/setup-kubectl@v4 + uses: azure/setup-kubectl@v5 - name: Install Helm - uses: azure/setup-helm@v4 + uses: azure/setup-helm@v5 with: version: '3.13.0' @@ -702,17 +801,17 @@ jobs: create-draft-release: name: Create Draft Release runs-on: ubuntu-latest - needs: [prepare, docker-merge, test-docker, test-binary, helm-package, test-helm, debian-build, rpm-build] + needs: [prepare, docker-merge, docker-build-dockerhub, test-docker, test-binary, helm-package, test-helm, debian-build, rpm-build] permissions: contents: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Download all release artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: pattern: arc-* merge-multiple: true @@ -835,7 +934,7 @@ jobs: cat release-notes.md - name: Create draft release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v3 with: draft: true prerelease: false