ci-actions/docker-publish/action.yml
Andras Schmelczer f2a58b2fb3 docker-publish: use BuildKit (buildx) instead of legacy builder
The runners share one host Docker daemon across replicas; the legacy
builder corrupts layers under that concurrency. buildx builds in an
isolated buildkitd container. buildx is installed as a static binary,
keeping the no-marketplace-actions property.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:50:20 +01:00

139 lines
6.2 KiB
YAML

name: Build & publish Docker image
description: >-
Build a Docker image with BuildKit (buildx) and push it to this Forgejo
instance's container registry. Reimplements docker/login-action +
docker/metadata-action + docker/setup-buildx-action + docker/build-push-action
in plain shell using the docker CLI and a statically-installed buildx plugin —
no marketplace actions are fetched or run.
BuildKit (not the legacy builder) is required: the runners share one host
Docker daemon across replicas, and the legacy builder corrupts layers under
that concurrency ("failed to get layer ... layer does not exist"). buildx
builds in an isolated buildkitd container, which is robust against it.
inputs:
context:
description: Docker build context.
default: "."
dockerfile:
description: Path to the Dockerfile (relative to the workspace). Empty = <context>/Dockerfile.
default: ""
image-suffix:
description: Appended to the derived image name <host>/<owner>/<repo>, e.g. "-server".
default: ""
build-args:
description: Newline-separated KEY=VALUE pairs passed as --build-arg.
default: ""
push:
description: Whether to log in and push ("true"/"false"). Set false for pull-request builds.
default: "true"
token:
description: Registry password/token (e.g. secrets.FORGEJO_PACKAGE_TOKEN).
required: true
registry-host:
description: Override the registry host. Defaults to github.server_url (vars.CONTAINER_REGISTRY_HOST is honoured if set in the env).
default: ""
runs:
using: composite
steps:
- name: Build and publish
shell: bash
env:
INPUT_CONTEXT: ${{ inputs.context }}
INPUT_DOCKERFILE: ${{ inputs.dockerfile }}
INPUT_IMAGE_SUFFIX: ${{ inputs.image-suffix }}
INPUT_BUILD_ARGS: ${{ inputs.build-args }}
INPUT_PUSH: ${{ inputs.push }}
INPUT_TOKEN: ${{ inputs.token }}
INPUT_REGISTRY_HOST: ${{ inputs.registry-host }}
REPO: ${{ github.repository }}
OWNER: ${{ github.repository_owner }}
SERVER_URL: ${{ github.server_url }}
SHA: ${{ github.sha }}
REF: ${{ github.ref }}
REF_TYPE: ${{ github.ref_type }}
REF_NAME: ${{ github.ref_name }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
# --- Ensure the docker CLI exists (idempotent; the node:* runner image may lack it) ---
if ! command -v docker >/dev/null 2>&1; then
ARCH="$(uname -m)"
curl -fsSL --retry 3 --retry-connrefused \
"https://download.docker.com/linux/static/stable/${ARCH}/docker-27.5.1.tgz" \
| tar xz --strip-components=1 -C /usr/local/bin docker/docker
fi
docker --version
# --- Ensure the buildx plugin exists (static binary, not a marketplace action) ---
if ! docker buildx version >/dev/null 2>&1; then
case "$(uname -m)" in
x86_64) BX_ARCH=amd64 ;;
aarch64|arm64) BX_ARCH=arm64 ;;
*) BX_ARCH="$(uname -m)" ;;
esac
mkdir -p /usr/local/lib/docker/cli-plugins
curl -fsSL --retry 3 --retry-connrefused \
"https://github.com/docker/buildx/releases/download/v0.19.3/buildx-v0.19.3.linux-${BX_ARCH}" \
-o /usr/local/lib/docker/cli-plugins/docker-buildx
chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx
fi
docker buildx version
# A host-networked buildkit builder so it can reach the registry on 127.0.0.1
# (buildkit treats loopback registries as insecure/HTTP automatically). A fixed
# name lets concurrent jobs share one builder and its cache; the inspect||create
# guard tolerates the create race between simultaneous jobs.
docker buildx inspect ci >/dev/null 2>&1 \
|| docker buildx create --name ci --driver docker-container --driver-opt network=host >/dev/null 2>&1 \
|| true
docker buildx use ci
# --- Resolve the registry host (canonical form of the block copy-pasted across repos) ---
host="${INPUT_REGISTRY_HOST:-${CONTAINER_REGISTRY_HOST:-$SERVER_URL}}"
host="${host#https://}"; host="${host#http://}"; host="${host%/}"
# The job's docker daemon reaches the registry via the host-published port, not the
# internal service name; this is the same remap the old inline scripts performed.
[ "$host" = "forgejo:3000" ] && host="127.0.0.1:13000"
repo="$(printf '%s' "$REPO" | tr '[:upper:]' '[:lower:]')"
image="${host}/${repo}${INPUT_IMAGE_SUFFIX}"
echo "Target image: ${image}"
# --- Compute tags from the git ref (replaces docker/metadata-action) ---
tags=( -t "${image}:sha-$(printf '%s' "$SHA" | cut -c1-12)" )
if [ "$REF" = "refs/heads/${DEFAULT_BRANCH}" ]; then
tags+=( -t "${image}:latest" )
fi
if [ "$REF_TYPE" = "tag" ]; then
tags+=( -t "${image}:${REF_NAME}" )
if printf '%s' "$REF_NAME" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
v="${REF_NAME#v}"
tags+=( -t "${image}:${v}" -t "${image}:${v%.*}" -t "${image}:${v%%.*}" )
fi
fi
# --- Assemble extra build flags ---
extra=()
[ -n "${INPUT_DOCKERFILE}" ] && extra+=( -f "${INPUT_DOCKERFILE}" )
while IFS= read -r a; do
[ -n "$a" ] && extra+=( --build-arg "$a" )
done <<< "${INPUT_BUILD_ARGS}"
# --- Log in before building so buildx can push directly (skipped on PRs) ---
if [ "${INPUT_PUSH}" = "true" ]; then
printf '%s' "${INPUT_TOKEN}" | docker login "$host" -u "$OWNER" --password-stdin
output=( --push )
else
# Pull-request builds validate the Dockerfile without producing/pushing an image.
output=()
fi
# --- Build (and push when requested) with BuildKit ---
docker buildx build "${extra[@]}" "${output[@]}" \
--label "org.opencontainers.image.source=${SERVER_URL}/${REPO}" \
--label "org.opencontainers.image.revision=${SHA}" \
--label "org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
"${tags[@]}" \
"${INPUT_CONTEXT}"