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
203 changes: 203 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# =============================================================================
# GitHub Actions CD Workflow
# =============================================================================
# This workflow handles continuous deployment of container images.
#
# Triggers:
# - Push of a version tag (v*)
#
# Jobs:
# - build: server + migrate images to ghcr.io, one job per platform,
# pushed by digest
# - merge: assembles the per-platform digests into multi-arch
# (amd64/arm64) manifest lists
#
# Notes (why this differs from the Go template's CD):
# - The Go Containerfile cross-compiles trivially (CGO_ENABLED=0) in a
# single buildx call over both platforms. Here the Rust musl
# cross-compiler setup (base image, target triple, linker name) is
# arch-specific, and build-push-action cannot vary build-args per
# platform in one invocation. So each platform is built natively (the
# arm64 leg runs on a native arm64 runner — QEMU emulation is
# prohibitively slow for Rust release builds), pushed by digest, and
# the manifest list is assembled afterwards.
# - Compilation happens inside the Containerfile, so no Rust toolchain
# is installed on the runner.
# =============================================================================

name: CD

on:
push:
tags: ["v*"]

env:
REGISTRY: ghcr.io

jobs:
# ==========================================================================
# Job: Build Container Images (per platform, by digest)
# ==========================================================================
build:
name: Build ${{ matrix.platform }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
permissions:
packages: write
contents: read
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
platform_id: linux-amd64
runner: ubuntu-latest
rust_musl_tag: x86_64-musl
musl_target: x86_64-unknown-linux-musl
musl_gcc: x86_64-unknown-linux-musl-gcc
musl_target_underscore: x86_64_unknown_linux_musl
musl_target_upper: X86_64_UNKNOWN_LINUX_MUSL
- platform: linux/arm64
platform_id: linux-arm64
runner: ubuntu-24.04-arm
rust_musl_tag: aarch64-musl
musl_target: aarch64-unknown-linux-musl
musl_gcc: aarch64-unknown-linux-musl-gcc
musl_target_underscore: aarch64_unknown_linux_musl
musl_target_upper: AARCH64_UNKNOWN_LINUX_MUSL
outputs:
version: ${{ steps.meta.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Extract metadata
id: meta
run: |
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
COMMIT_SHA=$(git rev-parse --short HEAD)
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
{
echo "version=${VERSION}"
echo "commit_sha=${COMMIT_SHA}"
echo "build_time=${BUILD_TIME}"
} >> "$GITHUB_OUTPUT"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0

- name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push server image by digest
id: build-server
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: ./Containerfile
platforms: ${{ matrix.platform }}
push: true
outputs: type=image,name=${{ env.REGISTRY }}/${{ github.repository }},push-by-digest=true,name-canonical=true
build-args: |
RUST_MUSL_TAG=${{ matrix.rust_musl_tag }}
MUSL_TARGET=${{ matrix.musl_target }}
MUSL_GCC=${{ matrix.musl_gcc }}
MUSL_TARGET_UNDERSCORE=${{ matrix.musl_target_underscore }}
MUSL_TARGET_UPPER=${{ matrix.musl_target_upper }}
VERSION=${{ steps.meta.outputs.version }}
COMMIT_SHA=${{ steps.meta.outputs.commit_sha }}
BUILD_TIME=${{ steps.meta.outputs.build_time }}
cache-from: type=gha,scope=${{ matrix.platform }}-server
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}-server

- name: Build and push migrate image by digest
id: build-migrate
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: ./Containerfile.migrate
platforms: ${{ matrix.platform }}
push: true
outputs: type=image,name=${{ env.REGISTRY }}/${{ github.repository }}-migrate,push-by-digest=true,name-canonical=true
build-args: |
RUST_MUSL_TAG=${{ matrix.rust_musl_tag }}
MUSL_TARGET=${{ matrix.musl_target }}
MUSL_GCC=${{ matrix.musl_gcc }}
MUSL_TARGET_UNDERSCORE=${{ matrix.musl_target_underscore }}
MUSL_TARGET_UPPER=${{ matrix.musl_target_upper }}
VERSION=${{ steps.meta.outputs.version }}
COMMIT_SHA=${{ steps.meta.outputs.commit_sha }}
BUILD_TIME=${{ steps.meta.outputs.build_time }}
cache-from: type=gha,scope=${{ matrix.platform }}-migrate
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}-migrate

- name: Export digests
run: |
mkdir -p /tmp/digests/${{ matrix.platform_id }}
echo "${{ steps.build-server.outputs.digest }}" | sed 's/^sha256://' > "/tmp/digests/${{ matrix.platform_id }}/server"
echo "${{ steps.build-migrate.outputs.digest }}" | sed 's/^sha256://' > "/tmp/digests/${{ matrix.platform_id }}/migrate"

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: digests-${{ matrix.platform_id }}
path: /tmp/digests/*
retention-days: 1
if-no-files-found: error

# ==========================================================================
# Job: Merge Manifest Lists
# ==========================================================================
merge:
name: Merge Manifests
runs-on: ubuntu-latest
timeout-minutes: 10
needs: build
permissions:
packages: write
contents: read
steps:
- name: Download digests
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0

- name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Create multi-arch manifest lists
run: |
VERSION="${{ needs.build.outputs.version }}"
cd /tmp/digests
for component in server migrate; do
image="${{ env.REGISTRY }}/${{ github.repository }}"
if [ "$component" = migrate ]; then image="${image}-migrate"; fi
refs=()
while IFS= read -r digest; do
refs+=("$image@sha256:$digest")
done < <(cat "linux-amd64/$component" "linux-arm64/$component")
docker buildx imagetools create \
-t "$image:$VERSION" \
-t "$image:latest" \
-t "$image:${{ github.ref_name }}" \
"${refs[@]}"
done

- name: Inspect resulting manifests
run: |
docker buildx imagetools inspect "${{ env.REGISTRY }}/${{ github.repository }}:latest"
docker buildx imagetools inspect "${{ env.REGISTRY }}/${{ github.repository }}-migrate:latest"
99 changes: 71 additions & 28 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
components: rustfmt
- name: cargo fmt --check
Expand All @@ -36,41 +38,70 @@ jobs:
timeout-minutes: 15
needs: fmt
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Install protoc
uses: arduino/setup-protoc@v3
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
with:
version: "27.1"
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: ${{ runner.os }}-clippy
- name: cargo clippy
run: cargo clippy --all-targets --locked -- -D warnings

architecture:
# Executable clean-architecture dependency gates (tests/architecture.rs):
# every dependency must point inward; the api facade is outward-only.
name: Architecture
runs-on: ubuntu-latest
timeout-minutes: 15
needs: clippy
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Install protoc
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
with:
version: "27.1"
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: ${{ runner.os }}-arch
- name: Layering rules (dependencies point inward only)
run: cargo test --test architecture --locked

unit:
name: Unit tests
runs-on: ubuntu-latest
timeout-minutes: 20
needs: clippy
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Install protoc
uses: arduino/setup-protoc@v3
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
with:
version: "27.1"
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
components: rustfmt, clippy, llvm-tools-preview
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: ${{ runner.os }}-unit
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
uses: taiki-e/install-action@6a241a1328ca6173d49760e1de6702cd5e52ef94 # cargo-llvm-cov
- name: Build test artifacts
# Ensures the coverage run below measures binaries too.
run: cargo build --all-targets --locked
Expand All @@ -85,9 +116,14 @@ jobs:
run: cargo llvm-cov report --html
- name: Coverage gate (60%)
env:
THRESHOLD: '60'
THRESHOLD: "60"
run: cargo llvm-cov report --fail-under-lines "$THRESHOLD"
- uses: actions/upload-artifact@v7
- uses: codecov/codecov-action@e53489f4d376d79066609109e7a95a29eb3740b1 # v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.lcov
fail_ci_if_error: false
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-report
path: |
Expand All @@ -100,7 +136,7 @@ jobs:
name: Integration tests
runs-on: ubuntu-latest
timeout-minutes: 25
needs: unit
needs: [unit, architecture]
services:
postgres:
image: docker.io/library/postgres:18-alpine
Expand Down Expand Up @@ -128,51 +164,58 @@ jobs:
env:
APP_ENVIRONMENT: test
DB_HOST: localhost
DB_PORT: '5432'
DB_PORT: "5432"
DB_NAME: zercle_template_test
DB_USER: postgres
DB_PASSWORD: postgres
DB_SSL_MODE: disable
DATABASE_URL: postgres://postgres:postgres@localhost:5432/zercle_template_test
VALKEY_HOST: localhost
VALKEY_PORT: '6379'
VALKEY_DB: '0'
VALKEY_PORT: "6379"
VALKEY_DB: "0"
RUST_LOG: info,sqlx=warn
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Install protoc
uses: arduino/setup-protoc@v3
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
with:
version: "27.1"
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: ${{ runner.os }}-integration
- name: Run migrations
run: cargo run --locked --bin migrate -- up
- name: Run integration tests
# Wave 4 / wave 7 #[ignore]-gated tests live under --ignored.
run: cargo test --all-targets --locked -- --ignored --test-threads=1
# Full suite against the service containers: the #[ignore]-gated
# live-infra tests (db, valkey, adapter roundtrip) plus the
# self-skipping HTTP integration test and the e2e server test,
# which no longer skip because infra IS reachable here.
run: cargo test --all-targets --locked -- --include-ignored --test-threads=1

build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [clippy, unit]
needs: [clippy, unit, architecture]
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Install protoc
uses: arduino/setup-protoc@v3
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
with:
version: "27.1"
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: ${{ runner.os }}-build
- name: cargo build --release (both bins)
Expand Down
Loading
Loading