From 4120f820a0977966fde1c1251af8a9719bcc6ec9 Mon Sep 17 00:00:00 2001 From: Mathias Beaulieu-Duncan Date: Mon, 2 Feb 2026 17:55:22 -0500 Subject: [PATCH] Fix multi-arch builds to preserve OCI config metadata Extract rootfs layer and config from apko OCI archive separately, then generate Dockerfile with ENV, ENTRYPOINT, WORKDIR and USER from the OCI config. Fixes missing environment variables in the final multi-arch image. Co-Authored-By: Claude Opus 4.5 --- .gitea/workflows/publish.yaml | 46 +++++++++++++++++++++++++++++------ .gitea/workflows/rebuild.yaml | 46 +++++++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 14 deletions(-) diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml index 71bf753..152043d 100644 --- a/.gitea/workflows/publish.yaml +++ b/.gitea/workflows/publish.yaml @@ -122,13 +122,45 @@ jobs: apko build --arch aarch64 ${{ matrix.config }} \ ${{ secrets.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ matrix.variant }}-${{ steps.tag.outputs.suffix }} \ /tmp/build-arm64/image.tar - cat > /tmp/Dockerfile <<'EOF' - FROM scratch - ARG TARGETARCH - ADD build-${TARGETARCH}/image.tar / - USER 65532 - EOF - sed -i 's/^ //' /tmp/Dockerfile + + # Extract the rootfs layer from the OCI archive for each arch. + # apko outputs an OCI image tarball; we need to extract just the + # filesystem layer (.tar.gz) and the config metadata. + for arch in amd64 arm64; do + cd /tmp/build-${arch} + tar xf image.tar + # The .tar.gz file is the filesystem layer + ROOTFS=$(ls *.tar.gz 2>/dev/null | head -1) + mv "$ROOTFS" rootfs.tar.gz + # Extract ENV and ENTRYPOINT from the OCI config + MANIFEST_DIGEST=$(jq -r '.manifests[0].digest' index.json | sed 's/sha256://') + CONFIG_DIGEST=$(jq -r '.config.digest' "sha256:${MANIFEST_DIGEST}" | sed 's/sha256://') + cp "sha256:${CONFIG_DIGEST}" config.json + # Clean up OCI artifacts + rm -f image.tar index.json manifest.json oci-layout sha256:* + cd /tmp + done + + # Generate Dockerfile with metadata from the OCI config + # (use amd64 config as reference — env vars are the same for both arches) + ENV_LINES=$(jq -r '(.config.Env // [])[] | "ENV " + .' /tmp/build-amd64/config.json) + ENTRYPOINT=$(jq -r '(.config.Entrypoint // [])[]' /tmp/build-amd64/config.json | head -1) + USER_ID=$(jq -r '.config.User // "65532"' /tmp/build-amd64/config.json) + WORKDIR=$(jq -r '.config.WorkingDir // "/"' /tmp/build-amd64/config.json) + + { + echo "FROM scratch" + echo "ARG TARGETARCH" + echo "ADD build-\${TARGETARCH}/rootfs.tar.gz /" + if [ -n "$ENV_LINES" ]; then + echo "$ENV_LINES" + fi + if [ -n "$ENTRYPOINT" ] && [ "$ENTRYPOINT" != "null" ]; then + echo "ENTRYPOINT [\"${ENTRYPOINT}\"]" + fi + echo "WORKDIR ${WORKDIR}" + echo "USER ${USER_ID}" + } > /tmp/Dockerfile - name: Build and push with buildx (SBOM + provenance) uses: docker/build-push-action@v5 diff --git a/.gitea/workflows/rebuild.yaml b/.gitea/workflows/rebuild.yaml index 06ad821..9df01fb 100644 --- a/.gitea/workflows/rebuild.yaml +++ b/.gitea/workflows/rebuild.yaml @@ -114,13 +114,45 @@ jobs: apko build --arch aarch64 ${{ matrix.config }} \ ${{ secrets.REGISTRY_URL }}/${{ env.IMAGE_NAME }}:${{ matrix.variant }}-latest \ /tmp/build-arm64/image.tar - cat > /tmp/Dockerfile <<'EOF' - FROM scratch - ARG TARGETARCH - ADD build-${TARGETARCH}/image.tar / - USER 65532 - EOF - sed -i 's/^ //' /tmp/Dockerfile + + # Extract the rootfs layer from the OCI archive for each arch. + # apko outputs an OCI image tarball; we need to extract just the + # filesystem layer (.tar.gz) and the config metadata. + for arch in amd64 arm64; do + cd /tmp/build-${arch} + tar xf image.tar + # The .tar.gz file is the filesystem layer + ROOTFS=$(ls *.tar.gz 2>/dev/null | head -1) + mv "$ROOTFS" rootfs.tar.gz + # Extract ENV and ENTRYPOINT from the OCI config + MANIFEST_DIGEST=$(jq -r '.manifests[0].digest' index.json | sed 's/sha256://') + CONFIG_DIGEST=$(jq -r '.config.digest' "sha256:${MANIFEST_DIGEST}" | sed 's/sha256://') + cp "sha256:${CONFIG_DIGEST}" config.json + # Clean up OCI artifacts + rm -f image.tar index.json manifest.json oci-layout sha256:* + cd /tmp + done + + # Generate Dockerfile with metadata from the OCI config + # (use amd64 config as reference — env vars are the same for both arches) + ENV_LINES=$(jq -r '(.config.Env // [])[] | "ENV " + .' /tmp/build-amd64/config.json) + ENTRYPOINT=$(jq -r '(.config.Entrypoint // [])[]' /tmp/build-amd64/config.json | head -1) + USER_ID=$(jq -r '.config.User // "65532"' /tmp/build-amd64/config.json) + WORKDIR=$(jq -r '.config.WorkingDir // "/"' /tmp/build-amd64/config.json) + + { + echo "FROM scratch" + echo "ARG TARGETARCH" + echo "ADD build-\${TARGETARCH}/rootfs.tar.gz /" + if [ -n "$ENV_LINES" ]; then + echo "$ENV_LINES" + fi + if [ -n "$ENTRYPOINT" ] && [ "$ENTRYPOINT" != "null" ]; then + echo "ENTRYPOINT [\"${ENTRYPOINT}\"]" + fi + echo "WORKDIR ${WORKDIR}" + echo "USER ${USER_ID}" + } > /tmp/Dockerfile - name: Build and push with buildx (SBOM + provenance) uses: docker/build-push-action@v5