ci: use native arm64 runners and add CI Docker builds

- Use native arm64 runners (ubuntu-24.04-arm) instead of QEMU for
  Docker builds in both docker-build.yml and release.yml
- Push Docker images by digest and create multi-arch manifests,
  avoiding separate per-arch tagged images
- Add ci.yml as a thin wrapper calling docker-build.yml on push to
  main (GHCR only)
- Split GoReleaser builds per-arch with --split/--merge for native
  compilation
- Add docker_manifests to .goreleaser.yaml for multi-arch support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Denys Vitali 2026-03-08 10:06:17 +00:00
parent 81dfdf5f45
commit 1fc7ac4af1
4 changed files with 192 additions and 85 deletions

35
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,35 @@
name: CI - Docker Build
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
ref: ${{ steps.tag.outputs.ref }}
steps:
- id: tag
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "tag=pr-${{ github.event.number }}" >> "$GITHUB_OUTPUT"
echo "ref=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
else
echo "tag=sha-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
echo "ref=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
fi
docker:
needs: prepare
uses: ./.github/workflows/docker-build.yml
with:
tag: ${{ needs.prepare.outputs.tag }}
ref: ${{ needs.prepare.outputs.ref }}
push_dockerhub: false
permissions:
contents: read
packages: write

View file

@ -4,9 +4,19 @@ on:
workflow_call:
inputs:
tag:
description: "Release tag"
description: "Image tag (e.g. v1.0.0 or sha-abc1234)"
required: true
type: string
ref:
description: "Git ref to checkout (defaults to inputs.tag)"
required: false
type: string
default: ""
push_dockerhub:
description: "Also push to Docker Hub"
required: false
type: boolean
default: true
env:
GHCR_REGISTRY: ghcr.io
@ -16,24 +26,32 @@ env:
jobs:
build:
name: 🏗️ Build Docker Image
runs-on: ubuntu-latest
name: 🏗️ Build ${{ matrix.platform }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
suffix: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
suffix: arm64
steps:
# ── Checkout ──────────────────────────────
- name: 📥 Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ inputs.tag }}
ref: ${{ inputs.ref || inputs.tag }}
# ── Docker Buildx ─────────────────────────
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# ── Login to GHCR ─────────────────────────
- name: 🔑 Login to GitHub Container Registry
uses: docker/login-action@v3
with:
@ -41,36 +59,91 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# ── Login to Docker Hub ────────────────────
- name: 🔑 Login to Docker Hub
if: inputs.push_dockerhub
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# ── Metadata (tags & labels) ──────────────
- name: 🏷️ Prepare image tags
id: tags
shell: bash
run: |
tag="${{ inputs.tag }}"
echo "ghcr_tag=${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:${tag}" >> "$GITHUB_OUTPUT"
echo "ghcr_latest=${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:latest" >> "$GITHUB_OUTPUT"
echo "dockerhub_tag=${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}:${tag}" >> "$GITHUB_OUTPUT"
echo "dockerhub_latest=${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}:latest" >> "$GITHUB_OUTPUT"
# ── Build & Push ──────────────────────────
- name: 🚀 Build and push Docker image
- name: 🚀 Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ steps.tags.outputs.ghcr_tag }}
${{ steps.tags.outputs.ghcr_latest }}
${{ steps.tags.outputs.dockerhub_tag }}
${{ steps.tags.outputs.dockerhub_latest }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64,linux/riscv64
file: docker/Dockerfile
platforms: ${{ matrix.platform }}
outputs: type=image,"name=${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.suffix }}
cache-to: type=gha,mode=max,scope=${{ matrix.suffix }}
- name: 📤 Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: 📦 Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.suffix }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
name: 🔗 Create multi-arch manifest
runs-on: ubuntu-latest
needs: build
permissions:
contents: read
packages: write
steps:
- name: 📥 Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🔑 Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 🔑 Login to Docker Hub
if: inputs.push_dockerhub
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 🏷️ Create and push multi-arch manifest (GHCR)
shell: bash
working-directory: /tmp/digests
run: |
GHCR="${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}"
docker buildx imagetools create \
--tag "${GHCR}:${{ inputs.tag }}" \
--tag "${GHCR}:latest" \
$(printf "${GHCR}@sha256:%s " *)
- name: 🏷️ Create and push multi-arch manifest (Docker Hub)
if: inputs.push_dockerhub
shell: bash
working-directory: /tmp/digests
run: |
GHCR="${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}"
DH="${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}"
docker buildx imagetools create \
--tag "${DH}:${{ inputs.tag }}" \
--tag "${DH}:latest" \
$(printf "${GHCR}@sha256:%s " *)

View file

@ -17,11 +17,6 @@ on:
required: false
type: boolean
default: false
upload_tos:
description: "Upload to Volcengine TOS"
required: false
type: boolean
default: true
jobs:
create-tag:
@ -45,13 +40,21 @@ jobs:
git tag -a "$RELEASE_TAG" -m "Release $RELEASE_TAG"
git push origin "$RELEASE_TAG"
release:
name: GoReleaser Release
build-release:
name: GoReleaser Build (${{ matrix.arch }})
needs: create-tag
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
permissions:
contents: write
packages: write
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
steps:
- name: Checkout tag
uses: actions/checkout@v6
@ -65,36 +68,46 @@ jobs:
with:
go-version-file: go.mod
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Run GoReleaser
- name: Run GoReleaser (split)
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: ~> v2
args: release --clean
args: release --clean --split
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
GOVERSION: ${{ steps.setup-go.outputs.go-version }}
GOARCH: ${{ matrix.arch }}
merge-release:
name: GoReleaser Merge & Publish
needs: build-release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout tag
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.tag }}
- name: Setup Go from go.mod
id: setup-go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Run GoReleaser (merge)
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: ~> v2
args: continue --merge
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}
GOVERSION: ${{ steps.setup-go.outputs.go-version }}
- name: Apply release flags
@ -106,11 +119,15 @@ jobs:
--draft=${{ inputs.draft }} \
--prerelease=${{ inputs.prerelease }}
upload-tos:
name: Upload to TOS
needs: release
if: ${{ inputs.upload_tos }}
uses: ./.github/workflows/upload-tos.yml
docker:
name: Build & Push Docker Image
needs: create-tag
uses: ./.github/workflows/docker-build.yml
with:
tag: ${{ inputs.tag }}
ref: ${{ inputs.tag }}
push_dockerhub: true
permissions:
contents: read
packages: write
secrets: inherit

View file

@ -94,24 +94,6 @@ builds:
- goos: windows
goarch: arm
dockers_v2:
- id: picoclaw
dockerfile: docker/Dockerfile.goreleaser
extra_files:
- docker/entrypoint.sh
ids:
- picoclaw
images:
- "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw"
- "docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}"
tags:
- "{{ .Tag }}"
- "latest"
platforms:
- linux/amd64
- linux/arm64
- linux/riscv64
archives:
- formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`.