Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/buildkitd.toml

This file was deleted.

132 changes: 102 additions & 30 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,70 +46,142 @@ jobs:
run: |
cargo test --release

# One image build per architecture, on a native runner. We push by digest;
# a final `merge` job stitches the two digests into a multi-arch manifest list.
# Native arm64 runners avoid QEMU emulation, which dominated build time.
image:
name: 🐳 Publish Image
runs-on: ubuntu-latest
name: 🐳 Build (${{ matrix.platform }})
needs:
- test
outputs:
tag: ${{ steps.tag.outputs.tag }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- name: 🧷 Platform pair
run: |
platform="${{ matrix.platform }}"
echo "PLATFORM_PAIR=${platform//\//-}" >> "${GITHUB_ENV}"

- name: 🛎️ Checkout
uses: actions/checkout@v5

- name: 🗽 Free disk space
uses: ShubhamTatvamasi/free-disk-space-action@master

- name: 🏷 Get tag
id: tag
run: |
if [ "${{ github.event_name }}" -eq "push" ]
then
echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
else
echo "tag=ci-${{ github.run_number }}" >> "$GITHUB_OUTPUT"
fi

- name: 🐳 Login to DockerHub
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: 🛠️ Set up QEMU
uses: docker/setup-qemu-action@v3

- name: 🛠️ Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
buildkitd-config: .github/buildkitd.toml
platforms: linux/amd64,linux/arm64

- name: 🐳 Build and push
# PR validation: build the image but don't push. Cache scoped per arch so
# the two matrix entries don't clobber each other's GHA cache.
- name: 🐳 Build (PR validation)
if: github.event_name != 'push'
uses: docker/build-push-action@v6
with:
context: .
build-args: BASE_IMAGE=platzio/base:v8
push: ${{ github.event_name == 'push' }}
platforms: linux/amd64,linux/arm64
tags: ${{ env.DOCKER_REPO }}:${{ steps.tag.outputs.tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: ${{ matrix.platform }}
push: false
cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
cache-to: type=gha,mode=max,scope=build-${{ env.PLATFORM_PAIR }}

# Tag push: build and push by digest. The merge job below assembles the
# final tagged manifest.
- name: 🐳 Build & push by digest
if: github.event_name == 'push'
id: build
uses: docker/build-push-action@v6
with:
context: .
build-args: BASE_IMAGE=platzio/base:v8
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
cache-to: type=gha,mode=max,scope=build-${{ env.PLATFORM_PAIR }}

- name: 📤 Export digest
if: github.event_name == 'push'
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"

- name: 📦 Upload digest
if: github.event_name == 'push'
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

merge:
name: 🧬 Assemble manifest list
if: github.event_name == 'push'
needs:
- image
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- name: 🏷 Get tag
id: tag
run: |
echo "tag=${GITHUB_REF#refs/tags/}" >> "${GITHUB_OUTPUT}"

- name: 📥 Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true

- name: 🐳 Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: 🛠️ Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: 🧬 Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
-t "${DOCKER_REPO}:${{ steps.tag.outputs.tag }}" \
$(printf "${DOCKER_REPO}@sha256:%s " *)

- name: 🔍 Inspect image
run: |
docker buildx imagetools inspect "${DOCKER_REPO}:${{ steps.tag.outputs.tag }}"

release:
name: 🚀 Create Release
if: github.event_name == 'push'
runs-on: ubuntu-latest
needs:
- image
- merge
permissions:
contents: write
packages: write
steps:
- name: 🏗️ Generate OpenAPI schema
run: |
docker run --rm \
${{ env.DOCKER_REPO }}:${{ needs.image.outputs.tag }} \
${{ env.DOCKER_REPO }}:${{ needs.merge.outputs.tag }} \
/root/platz-api openapi schema \
> openapi.yaml

Expand All @@ -120,8 +192,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
draft: true
tag_name: ${{ needs.image.outputs.tag }}
release_name: ${{ needs.image.outputs.tag }}
tag_name: ${{ needs.merge.outputs.tag }}
release_name: ${{ needs.merge.outputs.tag }}

- name: 📦 Upload OpenAPI schema
uses: actions/upload-release-asset@v1
Expand Down
8 changes: 6 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 104 additions & 14 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,22 +1,112 @@
# Multi-stage build using cargo-chef for dep-only layer caching and per-arch
# cache mounts so amd64 and arm64 don't fight over the same target dir.
#
# Drives the workspace into a static musl binary so the runtime image (alpine-
# based platzio/base) doesn't need a libc. Architecture is selected via Docker
# Buildx' automatic TARGETARCH build arg — set platforms in the build invocation,
# not here.

ARG BASE_IMAGE
ARG RUST_IMAGE=rust:1-trixie

FROM rust:1-trixie AS builder
RUN apt-get update && \
apt-get install -y \
# ---------------------------------------------------------------------------
# 1. chef — Rust toolchain + musl tooling + cargo-chef. Shared by planner and
# builder so both layers reuse the same toolchain image.
# ---------------------------------------------------------------------------
FROM ${RUST_IMAGE} AS chef
RUN apt-get update && apt-get install -y --no-install-recommends \
musl \
musl-dev \
musl-tools
musl-tools \
&& rm -rf /var/lib/apt/lists/*
RUN cargo install cargo-chef --locked --version ^0.1
WORKDIR /build

FROM builder AS build
# ---------------------------------------------------------------------------
# 2. planner — strip the workspace down to a "recipe" describing the dep graph.
# This stage is invalidated by *any* source change, but it's cheap (no compile).
# ---------------------------------------------------------------------------
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

# ---------------------------------------------------------------------------
# 3. builder — cook deps from the recipe, then build the workspace. The cooked
# deps live in a buildkit cache mount keyed by TARGETARCH, so each architecture
# keeps its own warm target dir across CI runs.
# ---------------------------------------------------------------------------
FROM chef AS builder
ARG RELEASE_BUILD=1
ARG TARGETARCH

RUN set -eux; \
case "${TARGETARCH}" in \
amd64) target=x86_64-unknown-linux-musl ;; \
arm64) target=aarch64-unknown-linux-musl ;; \
*) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
esac; \
echo "${target}" > /target.txt; \
rustup target add "${target}"

COPY --from=planner /build/recipe.json recipe.json
RUN --mount=type=cache,id=platz-cargo-target-${TARGETARCH},target=/build/target,sharing=locked \
--mount=type=cache,id=platz-cargo-git,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,id=platz-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
set -eux; \
target="$(cat /target.txt)"; \
if [ "${RELEASE_BUILD}" = "1" ]; then \
cargo chef cook --release --target "${target}" --recipe-path recipe.json; \
else \
cargo chef cook --target "${target}" --recipe-path recipe.json; \
fi

COPY . .
RUN --mount=type=cache,id=platz-cargo-target-${TARGETARCH},target=/build/target,sharing=locked \
--mount=type=cache,id=platz-cargo-git,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,id=platz-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
set -eux; \
target="$(cat /target.txt)"; \
if [ "${RELEASE_BUILD}" = "1" ]; then \
cargo build --release --target "${target}"; \
out_dir="target/${target}/release"; \
else \
cargo build --target "${target}"; \
out_dir="target/${target}/debug"; \
fi; \
mkdir -p /out; \
find "${out_dir}" -maxdepth 1 -type f -executable -exec cp -v {} /out/ \;

# ---------------------------------------------------------------------------
# 4a. dev — debug-mode build kept in the Rust toolchain image so Tilt's
# live_update can run `cargo build` *inside* the container after syncing
# source. No cache mount on the build step: the warm /build/target dir
# survives into the resulting image and incremental rebuilds in the running
# container reuse it. Dynamic-linked debian runtime (libpq5) — the musl-
# static release path isn't useful when we're recompiling at runtime.
#
# Selected by `--target=dev` (the Tiltfile in platzio/dev sets this). Not
# referenced by any other stage, so default builds skip it.
# ---------------------------------------------------------------------------
FROM ${RUST_IMAGE} AS dev
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
libpq5 \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
RUN mkdir -p /build/outputs
COPY . /build
RUN --mount=type=cache,id=platz-backend-cargo-target,target=/build/target,sharing=locked \
--mount=type=cache,id=platz-backend-cargo-git,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,id=platz-backend-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
./scripts/container-build.sh "${RELEASE_BUILD}" "/build/outputs"

FROM $BASE_IMAGE
COPY . .
RUN cargo build --workspace --bins \
&& mkdir -p /root \
&& cp target/debug/platz-api /root/platz-api \
&& cp target/debug/platz-k8s-agent /root/platz-k8s-agent \
&& cp target/debug/platz-chart-discovery /root/platz-chart-discovery \
&& cp target/debug/platz-status-updates /root/platz-status-updates \
&& cp target/debug/platz-resource-sync /root/platz-resource-sync
WORKDIR /root

# ---------------------------------------------------------------------------
# 4. runtime — small base image carrying just the static musl binaries.
# ---------------------------------------------------------------------------
FROM ${BASE_IMAGE}

Check warning on line 110 in Dockerfile

View workflow job for this annotation

GitHub Actions / 🐳 Build (linux/amd64)

Default value for global ARG results in an empty or invalid base image name

InvalidDefaultArgInFrom: Default value for ARG ${BASE_IMAGE} results in empty or invalid base image name More info: https://docs.docker.com/go/dockerfile/rule/invalid-default-arg-in-from/

Check warning on line 110 in Dockerfile

View workflow job for this annotation

GitHub Actions / 🐳 Build (linux/arm64)

Default value for global ARG results in an empty or invalid base image name

InvalidDefaultArgInFrom: Default value for ARG ${BASE_IMAGE} results in empty or invalid base image name More info: https://docs.docker.com/go/dockerfile/rule/invalid-default-arg-in-from/
WORKDIR /root/
COPY --from=build /build/outputs/* /root/
COPY --from=builder /out/* /root/
Loading
Loading