From e2b70ac50a61135a8721e7a09b440445be527bce Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Tue, 25 Aug 2026 00:36:05 +0700 Subject: [PATCH] ci: add Dockerfile and Gitea Actions workflow for irtt-server Introduce a multi-platform build pipeline to automate the publication of a minimal, statically linked irtt-server image to Gitea and GHCR. --- .gitea/workflows/docker-publish.yml | 99 +++++++++++++++++++++++++++++ Dockerfile | 42 ++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 .gitea/workflows/docker-publish.yml create mode 100644 Dockerfile diff --git a/.gitea/workflows/docker-publish.yml b/.gitea/workflows/docker-publish.yml new file mode 100644 index 0000000..40d507e --- /dev/null +++ b/.gitea/workflows/docker-publish.yml @@ -0,0 +1,99 @@ +name: Publish irtt-server image + +on: + push: + tags: + # This repo tags each crate independently (irtt-proto/vX, irtt-client/vX, + # ...); only the app package's tag corresponds to this image. + - "irtt-rs/v*" + workflow_dispatch: + +env: + IMAGE_NAME: irtt-server + IMAGE_OWNER: lochnair + GITEA_REGISTRY: git.lochnair.net + GHCR_REGISTRY: ghcr.io + # Pinned so a floating "latest" release doesn't silently change the build. + CRANE_VERSION: v0.20.2 + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Determine image tags + id: meta + run: | + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + version="${GITHUB_REF_NAME#irtt-rs/v}" + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "tags=${version},latest" >> "$GITHUB_OUTPUT" + else + # Manual dispatch off a branch: tag by commit only, never touch + # "latest", so an ad-hoc build can't clobber the last release. + version="sha-$(git rev-parse --short HEAD)" + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "tags=${version}" >> "$GITHUB_OUTPUT" + fi + + # Cross-compilation is handled inside the Dockerfile via tonistiigi/xx + # (native builds, no emulation), but buildx still needs a driver that + # can assemble a multi-platform manifest list. + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GITEA_REGISTRY }} + # The automatic per-job token comes back unauthorized for package + # writes on this instance, so use a PAT (scope: write:package) instead. + # (Secret names can't start with GITEA_/GITHUB_, hence REGISTRY_*.) + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ secrets.GHCR_USERNAME }} + password: ${{ secrets.GHCR_TOKEN }} + + - name: Build tag list + id: tags + run: | + IFS=',' read -ra tag_list <<< "${{ steps.meta.outputs.tags }}" + gitea_tags="" + for t in "${tag_list[@]}"; do + gitea_tags="${gitea_tags}${GITEA_REGISTRY}/${IMAGE_OWNER}/${IMAGE_NAME}:${t}," + done + echo "gitea=${gitea_tags%,}" >> "$GITHUB_OUTPUT" + + # Gitea is the primary publish target: this is the only build. Every + # other registry gets an exact digest copy of this same image (see + # below), never a separate build, so the hash matches everywhere. + - name: Build and push to Gitea + id: build + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.tags.outputs.gitea }} + + - name: Install crane + run: | + mkdir -p "${HOME}/.local/bin" + curl -sL "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Linux_x86_64.tar.gz" \ + | tar -xz -C "${HOME}/.local/bin" crane + echo "${HOME}/.local/bin" >> "$GITHUB_PATH" + + - name: Duplicate image to GHCR (same digest, no rebuild) + run: | + source_ref="${GITEA_REGISTRY}/${IMAGE_OWNER}/${IMAGE_NAME}@${{ steps.build.outputs.digest }}" + IFS=',' read -ra tag_list <<< "${{ steps.meta.outputs.tags }}" + for t in "${tag_list[@]}"; do + crane copy "${source_ref}" "${GHCR_REGISTRY}/${IMAGE_OWNER}/${IMAGE_NAME}:${t}" + done diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bc6ec20 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +# syntax=docker/dockerfile:1 + +# Builds the irtt-server applet only, statically linked against musl, and +# ships it in a scratch image with no shell or libc. +# +# Cross-compiles for TARGETPLATFORM using tonistiigi/xx: the builder stage +# always runs natively on BUILDPLATFORM (no QEMU emulation) and xx-cargo +# picks the right musl target triple, sysroot, and linker for whatever +# platform buildx asked for. + +FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx + +FROM --platform=$BUILDPLATFORM rust:1.88-alpine AS builder + +COPY --from=xx / / + +RUN apk add --no-cache clang lld file + +ARG TARGETPLATFORM +RUN xx-apk add --no-cache musl-dev + +WORKDIR /build + +COPY Cargo.toml Cargo.lock ./ +COPY crates ./crates + +RUN xx-cargo build \ + --release \ + --locked \ + -p irtt-rs \ + --bin irtt-server \ + --no-default-features \ + --features server \ + --target-dir /build/target \ + && xx-verify /build/target/$(xx-cargo --print-target-triple)/release/irtt-server \ + && cp /build/target/$(xx-cargo --print-target-triple)/release/irtt-server /irtt-server + +FROM scratch + +COPY --from=builder /irtt-server /irtt-server + +ENTRYPOINT ["/irtt-server"]