From 9f9ada81505874a79e6a174196be7989dd582601 Mon Sep 17 00:00:00 2001 From: Ganonmaster Date: Thu, 9 Apr 2026 17:26:35 +0200 Subject: [PATCH] Add docker build, rewrite Actions for Gitea, and add healthcheck endpoint. --- .gitea/workflows/release.yml | 113 ++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 51 --------------- CLAUDE.md | 59 ++++++++++++++++++ Dockerfile | 37 +++++++++++ src/main.rs | 1 + src/routes/health.rs | 8 +++ src/routes/mod.rs | 1 + 7 files changed, 219 insertions(+), 51 deletions(-) create mode 100644 .gitea/workflows/release.yml delete mode 100644 .github/workflows/release.yml create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 src/routes/health.rs diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..479389e --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,113 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + packages: write + +jobs: + build-linux: + name: Build Linux release binary + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # needed so build.rs can run `git describe` + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-gnu + + - name: Cache Cargo registry and build artefacts + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Build release binary + run: cargo build --release --target x86_64-unknown-linux-gnu + + - name: Rename binary + run: | + cp target/x86_64-unknown-linux-gnu/release/mikebase \ + mikebase-${{ github.ref_name }}-x86_64-linux + + - name: Create Gitea release and upload binary + env: + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITEA_URL: ${{ github.server_url }} + REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} + run: | + # Create the release + RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${GITEA_URL}/api/v1/repos/${REPO}/releases" \ + -d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"draft\":false,\"prerelease\":false}") + + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | head -1) + + if [ "$HTTP_CODE" -ne 201 ]; then + echo "Failed to create release (HTTP $HTTP_CODE): $BODY" + exit 1 + fi + + RELEASE_ID=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") + + # Upload binary asset + BINARY="mikebase-${TAG}-x86_64-linux" + curl -s -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + "${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${BINARY}" \ + --data-binary @"${BINARY}" + + docker: + name: Build and push Docker image + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Log in to Gitea container registry + uses: docker/login-action@v3 + with: + registry: git.open3dlab.com + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: git.open3dlab.com/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 48aa498..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*" - -permissions: - contents: write - -jobs: - build-linux: - name: Build Linux release binary - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # needed so build.rs can run `git describe` - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - with: - targets: x86_64-unknown-linux-gnu - - - name: Cache Cargo registry and build artefacts - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry/index - ~/.cargo/registry/cache - ~/.cargo/git/db - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo- - - - name: Build release binary - run: cargo build --release --target x86_64-unknown-linux-gnu - - - name: Rename binary for upload - run: | - cp target/x86_64-unknown-linux-gnu/release/mikebase \ - mikebase-${{ github.ref_name }}-x86_64-linux - - - name: Upload binary to GitHub Release - uses: softprops/action-gh-release@v2 - with: - files: mikebase-${{ github.ref_name }}-x86_64-linux - generate_release_notes: true diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9a282b0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,59 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Build +cargo build +cargo build --release + +# Run (requires config.toml or APP__ env vars) +cargo run + +# Check without building +cargo check + +# Run tests +cargo test + +# Run a single test +cargo test + +# Lint +cargo clippy + +# Format +cargo fmt +``` + +## Configuration + +Copy `config.example.toml` to `config.toml` and fill in the values. All config fields can be overridden with `APP__` prefixed environment variables using `__` as separator (e.g. `APP__DATABASE__URL`, `APP__S3__BUCKET`). + +Supports both SQLite (`sqlite://mikebase.db`) and PostgreSQL (`postgresql://user:pass@host/db`) via `sqlx::AnyPool`. + +## Architecture + +The app is an Axum HTTP server with shared state (`AppState`) containing two components: +- **`Database`** (`src/db.rs`): Thin wrapper around `sqlx::AnyPool` supporting SQLite and PostgreSQL. Runs migrations from `migrations/` on startup. All timestamps are stored as ISO 8601 strings (not native DB types) because `sqlx::Any` lacks a blanket chrono `Encode`/`Decode` impl. +- **`S3Storage`** (`src/storage.rs`): AWS SDK S3 client pointed at a configurable endpoint (Wasabi by default). Images are uploaded under the key `emoji/` and public URLs are built from the configured `public_url` base. + +**Data flow for emote creation:** multipart form → upload bytes to S3 → insert row into DB → return `EmoteResponse` JSON. + +**Models** (`src/models.rs`): +- `EmoteRow` — raw DB row (timestamps as strings) +- `EmoteResponse` — JSON API response (timestamps as `DateTime`) + +**Routes** (`src/routes/`): +- `GET /` — health check +- `GET /version` — git commit hash and tag (baked in at build time via `build.rs`) +- `GET /json` — list all emotes +- `POST /emotes` — create emote (multipart: `name`, `alias?`, `file`) +- `PUT /emotes/{uuid}` — update emote metadata (JSON: `name?`, `alias?`, `image_key?`) +- `DELETE /emotes/{uuid}` — delete emote from DB and S3 (S3 delete is best-effort) + +## Releases + +Pushing a `v*` tag triggers the GitHub Actions release workflow, which builds a Linux binary and uploads it to a GitHub Release. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..10fcb92 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# ── Build stage ────────────────────────────────────────────────────────────── +FROM rust:1-slim AS builder + +WORKDIR /build + +# Cache dependency compilation separately from application code. +COPY Cargo.toml Cargo.lock build.rs ./ +# Dummy source so `cargo build` can compile dependencies without the real code. +RUN mkdir src && echo 'fn main(){}' > src/main.rs \ + && cargo build --release \ + && rm src/main.rs + +# Build the real application. +COPY src ./src +COPY migrations ./migrations +# Touch main.rs so Cargo detects the change and recompiles. +RUN touch src/main.rs \ + && cargo build --release + +# ── Runtime stage ───────────────────────────────────────────────────────────── +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY --from=builder /build/target/release/mikebase ./mikebase +COPY migrations ./migrations + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3000/health || exit 1 + +ENTRYPOINT ["./mikebase"] diff --git a/src/main.rs b/src/main.rs index c895e51..0677932 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,6 +54,7 @@ async fn main() { let app = Router::new() .route("/", get(routes::emotes::root)) + .route("/health", get(routes::health::health)) .route("/version", get(routes::version::version)) .route("/json", get(routes::emotes::list_emotes)) .route("/emotes", post(routes::emotes::create_emote)) diff --git a/src/routes/health.rs b/src/routes/health.rs new file mode 100644 index 0000000..0f9e111 --- /dev/null +++ b/src/routes/health.rs @@ -0,0 +1,8 @@ +use axum::response::{IntoResponse, Json}; +use serde_json::json; + +/// GET /health +/// Returns a simple health-check response for liveness probes. +pub async fn health() -> impl IntoResponse { + Json(json!({"status": "ok"})) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 39bf7e9..4cb0bc3 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,2 +1,3 @@ pub mod emotes; +pub mod health; pub mod version;