From 6d3e99220cce2a69e6f5a3b0dd132f3ad85bc633 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar Date: Wed, 18 Feb 2026 09:46:29 +0000 Subject: [PATCH] ci: added basic workflows (#2) Co-authored-by: Sharang Parnerkar Reviewed-on: https://gitea.meghsakha.com/sharang/certifai/pulls/2 --- .gitea/workflows/ci.yml | 177 ++++++++++++++++++++++++++++++++++++ Dockerfile | 61 +++++++++++++ bin/main.rs | 1 - build.rs | 15 ++- cliff.toml | 52 +++++++++++ src/components/app_shell.rs | 4 +- src/components/card.rs | 7 +- src/components/sidebar.rs | 23 +---- src/infrastructure/auth.rs | 4 +- src/infrastructure/state.rs | 6 +- src/pages/overview.rs | 24 +---- 11 files changed, 316 insertions(+), 58 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 Dockerfile create mode 100644 cliff.toml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..9a663a2 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,177 @@ +name: CI + +on: + push: + branches: + - "**" + pull_request: + branches: + - main + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-D warnings" + +# Cancel in-progress runs for the same branch/PR +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # --------------------------------------------------------------------------- + # Stage 1: Code quality checks (run in parallel) + # --------------------------------------------------------------------------- + fmt: + name: Format + runs-on: docker + container: + image: rust:1.89-bookworm + steps: + - name: Checkout + run: | + git init + git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" + git fetch --depth=1 origin "${GITHUB_SHA}" + git checkout FETCH_HEAD + - run: rustup component add rustfmt + - run: cargo fmt --check + - name: Install dx CLI + run: cargo install dioxus-cli@0.7.3 --locked + - name: RSX format check + run: dx fmt --check + + clippy: + name: Clippy + runs-on: docker + container: + image: rust:1.89-bookworm + steps: + - name: Checkout + run: | + git init + git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" + git fetch --depth=1 origin "${GITHUB_SHA}" + git checkout FETCH_HEAD + - run: rustup component add clippy + # Lint both feature sets independently + - name: Clippy (server) + run: cargo clippy --features server --no-default-features -- -D warnings + - name: Clippy (web) + run: cargo clippy --features web --no-default-features -- -D warnings + + audit: + name: Security Audit + runs-on: docker + container: + image: rust:1.89-bookworm + steps: + - name: Checkout + run: | + git init + git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" + git fetch --depth=1 origin "${GITHUB_SHA}" + git checkout FETCH_HEAD + - run: cargo install cargo-audit + - run: cargo audit + + # --------------------------------------------------------------------------- + # Stage 2: Tests (only after all quality checks pass) + # --------------------------------------------------------------------------- + test: + name: Tests + runs-on: docker + needs: [fmt, clippy, audit] + container: + image: rust:1.89-bookworm + steps: + - name: Checkout + run: | + git init + git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" + git fetch --depth=1 origin "${GITHUB_SHA}" + git checkout FETCH_HEAD + - name: Run tests (server) + run: cargo test --features server --no-default-features + - name: Run tests (web) + run: cargo test --features web --no-default-features + + # --------------------------------------------------------------------------- + # Stage 3: Build Docker image and push to registry + # Only on main and release/* branches + # --------------------------------------------------------------------------- + build-and-push: + name: Build & Push Image + runs-on: docker + needs: [test] + if: >- + github.event_name == 'push' && + (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) + steps: + - name: Checkout + run: | + git init + git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" + git fetch --depth=1 origin "${GITHUB_SHA}" + git checkout FETCH_HEAD + + - name: Determine image tag + id: tag + run: | + BRANCH="${GITHUB_REF#refs/heads/}" + # Replace / with - for valid Docker tags (e.g. release/1.0 -> release-1.0) + BRANCH_SAFE=$(echo "$BRANCH" | tr '/' '-') + SHA=$(echo "$GITHUB_SHA" | head -c 8) + echo "tag=${BRANCH_SAFE}-${SHA}" >> "$GITHUB_OUTPUT" + + - name: Log in to container registry + run: >- + echo "${{ secrets.REGISTRY_PASSWORD }}" + | docker login https://registry.meghsakha.com + -u "${{ secrets.REGISTRY_USERNAME }}" + --password-stdin + + - name: Build Docker image + run: >- + docker build + -t registry.meghsakha.com/certifai/dashboard:${{ steps.tag.outputs.tag }} + -t registry.meghsakha.com/certifai/dashboard:latest + . + + - name: Push Docker image + run: | + docker push registry.meghsakha.com/certifai/dashboard:${{ steps.tag.outputs.tag }} + docker push registry.meghsakha.com/certifai/dashboard:latest + + # --------------------------------------------------------------------------- + # Stage 3b: Generate changelog from conventional commits + # Only on main and release/* branches + # --------------------------------------------------------------------------- + changelog: + name: Changelog + runs-on: docker + needs: [test] + if: >- + github.event_name == 'push' && + (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) + container: + image: rust:1.89-bookworm + steps: + - name: Checkout (full history) + run: | + git clone "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" . + git checkout "${GITHUB_SHA}" + - name: Install git-cliff + run: cargo install git-cliff --locked + - name: Generate changelog + run: git cliff --output CHANGELOG.md + - name: Commit and push changelog + run: | + git config user.name "CI Bot" + git config user.email "ci@certifai.local" + git add CHANGELOG.md + if git diff --cached --quiet; then + echo "No changelog changes to commit" + else + git commit -m "docs: update CHANGELOG.md [skip ci]" + git push origin HEAD:"${GITHUB_REF_NAME}" + fi diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4880070 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +# Stage 1: Generate dependency recipe for caching +FROM rust:1.89-bookworm AS chef +RUN cargo install cargo-chef +WORKDIR /app + +FROM chef AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +# Stage 2: Build dependencies + application +FROM chef AS builder + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + pkg-config libssl-dev curl unzip \ + && rm -rf /var/lib/apt/lists/* + +# Install bun (for Tailwind CSS build step) +RUN curl -fsSL https://bun.sh/install | bash +ENV PATH="/root/.bun/bin:$PATH" + +# Install dx CLI via cargo-binstall +RUN curl -L --proto '=https' --tlsv1.2 -sSf \ + https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh \ + | bash +RUN cargo binstall dioxus-cli@0.7.3 --root /.cargo -y --force +ENV PATH="/.cargo/bin:$PATH" + +# Cook dependencies from recipe (cached layer) +COPY --from=planner /app/recipe.json recipe.json +RUN cargo chef cook --release --recipe-path recipe.json + +# Copy source and build +COPY . . + +# Ensure styles directory exists for build.rs tailwind step +RUN mkdir -p styles && touch styles/input.css + +# Bundle the fullstack application +RUN dx bundle --platform fullstack + +# Stage 3: Minimal runtime image +FROM debian:bookworm-slim AS runtime + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates libssl3 \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd --create-home --shell /bin/bash app +USER app +WORKDIR /home/app + +# Copy the bundled output from builder +COPY --from=builder --chown=app:app /app/target/dx/dashboard/release/web/ ./ + +EXPOSE 8000 + +ENV IP=0.0.0.0 +ENV PORT=8000 + +ENTRYPOINT ["./server"] diff --git a/bin/main.rs b/bin/main.rs index 777c35c..88d689b 100644 --- a/bin/main.rs +++ b/bin/main.rs @@ -1,6 +1,5 @@ #![allow(non_snake_case)] #[allow(clippy::expect_used)] - fn main() { // Init logger dioxus_logger::init(tracing::Level::DEBUG).expect("Failed to init logger"); diff --git a/build.rs b/build.rs index 4cfbc39..521a6cb 100644 --- a/build.rs +++ b/build.rs @@ -1,8 +1,9 @@ -#[allow(clippy::expect_used)] fn main() -> Result<(), Box> { use std::process::Command; println!("cargo:rerun-if-changed=./styles/input.css"); - Command::new("bunx") + + // Tailwind build is optional - skip gracefully in CI or environments without bun + match Command::new("bunx") .args([ "@tailwindcss/cli", "-i", @@ -11,7 +12,15 @@ fn main() -> Result<(), Box> { "./assets/tailwind.css", ]) .status() - .expect("could not run tailwind"); + { + Ok(status) if !status.success() => { + println!("cargo:warning=tailwind build exited with {status}, skipping CSS generation"); + } + Err(e) => { + println!("cargo:warning=bunx not found ({e}), skipping tailwind CSS generation"); + } + Ok(_) => {} + } Ok(()) } diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..79805c7 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,52 @@ +[changelog] +header = """ +# Changelog + +All notable changes to this project will be documented in this file. + +""" +body = """ +{%- macro remote_url() -%} + https://gitea.meghsakha.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} + +{% if version -%} + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else -%} + ## [Unreleased] +{% endif -%} + +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}{{ commit.message | upper_first }}\ + {% if commit.breaking %} [**breaking**]{% endif %}\ + {% endfor %} +{% endfor %}\n +""" +footer = """ +--- +*Generated by [git-cliff](https://git-cliff.org)* +""" +trim = true + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^style", group = "Styling" }, + { message = "^test", group = "Testing" }, + { message = "^ci", group = "CI/CD" }, + { message = "^chore", group = "Miscellaneous" }, + { message = "^build", group = "Build" }, +] +protect_breaking_commits = false +filter_commits = false +tag_pattern = "v[0-9].*" +sort_commits = "oldest" diff --git a/src/components/app_shell.rs b/src/components/app_shell.rs index 3763225..12ec446 100644 --- a/src/components/app_shell.rs +++ b/src/components/app_shell.rs @@ -15,9 +15,7 @@ pub fn AppShell() -> Element { email: "user@example.com".to_string(), avatar_url: String::new(), } - main { class: "main-content", - Outlet:: {} - } + main { class: "main-content", Outlet:: {} } } } } diff --git a/src/components/card.rs b/src/components/card.rs index 4461de6..58e4803 100644 --- a/src/components/card.rs +++ b/src/components/card.rs @@ -9,12 +9,7 @@ use dioxus::prelude::*; /// * `href` - URL the card links to when clicked. /// * `icon` - Element rendered as the card icon (typically a `dioxus_free_icons::Icon`). #[component] -pub fn DashboardCard( - title: String, - description: String, - href: String, - icon: Element, -) -> Element { +pub fn DashboardCard(title: String, description: String, href: String, icon: Element) -> Element { rsx! { a { class: "dashboard-card", href: "{href}", div { class: "card-icon", {icon} } diff --git a/src/components/sidebar.rs b/src/components/sidebar.rs index 6da3675..c4fbe8a 100644 --- a/src/components/sidebar.rs +++ b/src/components/sidebar.rs @@ -1,7 +1,6 @@ use dioxus::prelude::*; use dioxus_free_icons::icons::bs_icons::{ - BsBoxArrowRight, BsFileEarmarkText, BsGear, BsGithub, BsGrid, - BsHouseDoor, BsRobot, + BsBoxArrowRight, BsFileEarmarkText, BsGear, BsGithub, BsGrid, BsHouseDoor, BsRobot, }; use dioxus_free_icons::icons::fa_solid_icons::FaCubes; use dioxus_free_icons::Icon; @@ -66,15 +65,9 @@ pub fn Sidebar(email: String, avatar_url: String) -> Element { { // Simple active check: highlight Overview only when on `/`. let is_active = item.route == current_route; - let cls = if is_active { - "sidebar-link active" - } else { - "sidebar-link" - }; + let cls = if is_active { "sidebar-link active" } else { "sidebar-link" }; rsx! { - Link { - to: item.route, - class: cls, + Link { to: item.route, class: cls, {item.icon} span { "{item.label}" } } @@ -135,16 +128,10 @@ fn SidebarFooter() -> Element { rsx! { footer { class: "sidebar-footer", div { class: "sidebar-social", - a { - href: "#", - class: "social-link", - title: "GitHub", + a { href: "#", class: "social-link", title: "GitHub", Icon { icon: BsGithub, width: 16, height: 16 } } - a { - href: "#", - class: "social-link", - title: "Impressum", + a { href: "#", class: "social-link", title: "Impressum", Icon { icon: BsGrid, width: 16, height: 16 } } } diff --git a/src/infrastructure/auth.rs b/src/infrastructure/auth.rs index 110038c..bf8538a 100644 --- a/src/infrastructure/auth.rs +++ b/src/infrastructure/auth.rs @@ -214,7 +214,7 @@ pub async fn auth_callback( let client = reqwest::Client::new(); let token_resp = client - .post(&config.token_endpoint()) + .post(config.token_endpoint()) .form(&[ ("grant_type", "authorization_code"), ("client_id", &config.client_id), @@ -237,7 +237,7 @@ pub async fn auth_callback( // --- Fetch userinfo --- let userinfo: UserinfoResponse = client - .get(&config.userinfo_endpoint()) + .get(config.userinfo_endpoint()) .bearer_auth(&tokens.access_token) .send() .await diff --git a/src/infrastructure/state.rs b/src/infrastructure/state.rs index f83b157..89d77bc 100644 --- a/src/infrastructure/state.rs +++ b/src/infrastructure/state.rs @@ -1,11 +1,7 @@ -use std::{ - ops::{Deref, DerefMut}, - sync::Arc, -}; +use std::{ops::Deref, sync::Arc}; use axum::extract::FromRequestParts; use serde::{Deserialize, Serialize}; -use tracing::debug; #[derive(Debug, Clone)] pub struct UserState(Arc); diff --git a/src/pages/overview.rs b/src/pages/overview.rs index bda29fa..3f5c1b8 100644 --- a/src/pages/overview.rs +++ b/src/pages/overview.rs @@ -40,11 +40,7 @@ pub fn OverviewPage() -> Element { description: "Guides & API Reference".to_string(), href: "#".to_string(), icon: rsx! { - Icon { - icon: BsBook, - width: 28, - height: 28, - } + Icon { icon: BsBook, width: 28, height: 28 } }, } DashboardCard { @@ -52,11 +48,7 @@ pub fn OverviewPage() -> Element { description: "Observability & Analytics".to_string(), href: "#".to_string(), icon: rsx! { - Icon { - icon: FaChartLine, - width: 28, - height: 28, - } + Icon { icon: FaChartLine, width: 28, height: 28 } }, } DashboardCard { @@ -64,11 +56,7 @@ pub fn OverviewPage() -> Element { description: "Agent Framework".to_string(), href: "#".to_string(), icon: rsx! { - Icon { - icon: FaGears, - width: 28, - height: 28, - } + Icon { icon: FaGears, width: 28, height: 28 } }, } DashboardCard { @@ -76,11 +64,7 @@ pub fn OverviewPage() -> Element { description: "Browse Models".to_string(), href: "#".to_string(), icon: rsx! { - Icon { - icon: FaCubes, - width: 28, - height: 28, - } + Icon { icon: FaCubes, width: 28, height: 28 } }, } }