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
This commit is contained in:
Tracewebstudio Dev 2026-04-12 21:57:28 +02:00
parent 019613aa82
commit dade35b328
5 changed files with 136 additions and 1 deletions

View file

@ -46,3 +46,31 @@ steps:
skip_tls_verify: true skip_tls_verify: true
platforms: linux/amd64 platforms: linux/amd64
cache: false 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

View file

@ -24,7 +24,8 @@ members = [
"crates/email", "crates/email",
"apps/cron", "apps/cron",
"apps/employees", "apps/employees",
"apps/payments" "apps/payments",
"crates/db-migrate"
] ]
[workspace.package] [workspace.package]

22
Dockerfile.migrate Normal file
View file

@ -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"]

View file

@ -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 }

View file

@ -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(())
}