From dade35b328554181797bf4e64d2f20d936b8c7e6 Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Sun, 12 Apr 2026 21:57:28 +0200 Subject: [PATCH] feat: add db-migrate tool for running SQL migrations - Create db-migrate binary that runs all .up.sql migration files - Add Dockerfile.migrate for building the migration image - Add migration job to Woodpecker CI pipeline - Image will be pushed to registry.nxtgauge.com:5000/nxtgauge-db-migrate --- .woodpecker.yml | 28 ++++++++++++++ Cargo.toml | 3 +- Dockerfile.migrate | 22 +++++++++++ crates/db-migrate/Cargo.toml | 12 ++++++ crates/db-migrate/src/main.rs | 72 +++++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 Dockerfile.migrate create mode 100644 crates/db-migrate/Cargo.toml create mode 100644 crates/db-migrate/src/main.rs diff --git a/.woodpecker.yml b/.woodpecker.yml index 1acba5b..16e9e3b 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -46,3 +46,31 @@ steps: skip_tls_verify: true platforms: linux/amd64 cache: false + +--- +when: + branch: [main, high-performance] + event: push + +steps: + - name: build-and-push-migrate + image: woodpeckerci/plugin-kaniko:2.1.1 + settings: + registry: + from_secret: REGISTRY_HOSTPORT + repo: nxtgauge-db-migrate + dockerfile: Dockerfile.migrate + context: . + tags: + - ${CI_COMMIT_SHA} + - latest + - high-performance-latest + username: + from_secret: REGISTRY_USERNAME + password: + from_secret: REGISTRY_PASSWORD + insecure: true + insecure_pull: true + skip_tls_verify: true + platforms: linux/amd64 + cache: false diff --git a/Cargo.toml b/Cargo.toml index 1ccceb7..8c954d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,8 @@ members = [ "crates/email", "apps/cron", "apps/employees", - "apps/payments" + "apps/payments", + "crates/db-migrate" ] [workspace.package] diff --git a/Dockerfile.migrate b/Dockerfile.migrate new file mode 100644 index 0000000..c5ef565 --- /dev/null +++ b/Dockerfile.migrate @@ -0,0 +1,22 @@ +FROM rust:1.75-alpine AS builder + +WORKDIR /app + +RUN apk add --no-cache musl-dev pkgconfig openssl-dev + +COPY Cargo.toml Cargo.lock ./ +COPY crates/db-migrate ./crates/db-migrate +COPY crates/db ./crates/db +COPY crates/cache ./crates/cache +COPY crates/email ./crates/email + +WORKDIR /app/crates/db-migrate +RUN cargo build --release --bin db-migrate + +FROM alpine:3.19 +RUN apk add --no-cache ca-certificates libpq + +COPY --from=builder /app/crates/db-migrate/target/release/db-migrate /usr/local/bin/ +COPY crates/db/migrations /migrations + +ENTRYPOINT ["db-migrate"] diff --git a/crates/db-migrate/Cargo.toml b/crates/db-migrate/Cargo.toml new file mode 100644 index 0000000..033c9ac --- /dev/null +++ b/crates/db-migrate/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "db-migrate" +version = "0.1.0" +edition = "2021" + +[dependencies] +sqlx = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } +anyhow = { workspace = true } +tokio = { workspace = true, features = ["full"] } +serde = { workspace = true } diff --git a/crates/db-migrate/src/main.rs b/crates/db-migrate/src/main.rs new file mode 100644 index 0000000..343171b --- /dev/null +++ b/crates/db-migrate/src/main.rs @@ -0,0 +1,72 @@ +use std::path::Path; +use anyhow::{Context, Result}; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::new( + std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()), + )) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let database_url = std::env::var("DATABASE_URL") + .context("DATABASE_URL must be set")?; + + tracing::info!("Connecting to database..."); + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(1) + .connect(&database_url) + .await + .context("Failed to connect to database")?; + tracing::info!("Connected to database"); + + let migrations_dir = std::env::var("MIGRATIONS_DIR") + .unwrap_or_else(|_| "/migrations".to_string()); + + run_migrations(&pool, &migrations_dir).await?; + + tracing::info!("All migrations completed successfully!"); + Ok(()) +} + +async fn run_migrations(pool: &sqlx::PgPool, migrations_dir: &str) -> Result<()> { + let migrations_path = Path::new(migrations_dir); + + if !migrations_path.exists() { + tracing::warn!("Migrations directory does not exist: {}", migrations_dir); + return Ok(()); + } + + let mut entries: Vec<_> = std::fs::read_dir(migrations_path)? + .filter_map(|e| e.ok()) + .filter(|e| { + let name = e.file_name(); + let name_str = name.to_string_lossy(); + name_str.ends_with(".up.sql") + }) + .collect(); + + entries.sort_by_key(|e| e.file_name()); + + tracing::info!("Found {} migration files", entries.len()); + + for entry in entries { + let file_name = entry.file_name(); + let file_path = entry.path(); + + tracing::info!("Applying migration: {}", file_name.to_string_lossy()); + + let sql = std::fs::read_to_string(&file_path) + .with_context(|| format!("Failed to read migration: {}", file_name.to_string_lossy()))?; + + sqlx::raw_sql(&sql) + .execute(pool) + .await + .with_context(|| format!("Failed to execute migration: {}", file_name.to_string_lossy()))?; + + tracing::info!("Applied migration: {}", file_name.to_string_lossy()); + } + + Ok(()) +}