feat: rename service

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
2026-02-27 13:41:04 +01:00
parent 749ae245c7
commit 444c3d760b
8 changed files with 1328 additions and 107 deletions

11
ci/Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "ci"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
dagger-sdk = "0.20"
eyre = "0.6"
tokio = { version = "1", features = ["full"] }
clap = { version = "4", features = ["derive"] }

274
ci/src/main.rs Normal file
View File

@@ -0,0 +1,274 @@
use std::path::PathBuf;
use clap::Parser;
const BIN_NAME: &str = "sq-server";
const MOLD_VERSION: &str = "2.40.4";
#[derive(Parser)]
#[command(name = "ci")]
enum Cli {
/// Run PR validation pipeline (check + test + build)
Pr,
/// Run main branch pipeline (check + test + build)
Main,
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
let cli = Cli::parse();
dagger_sdk::connect(|client| async move {
match cli {
Cli::Pr => run_pr(&client).await?,
Cli::Main => run_main(&client).await?,
}
Ok(())
})
.await?;
Ok(())
}
async fn run_pr(client: &dagger_sdk::Query) -> eyre::Result<()> {
eprintln!("==> PR pipeline: check + test + build");
let base = build_base(client).await?;
eprintln!("--- cargo check --workspace");
base.clone()
.with_exec(vec!["cargo", "check", "--workspace"])
.sync()
.await?;
eprintln!("--- running tests");
run_tests(&base).await?;
eprintln!("--- building release image");
let _image = build_release_image(client, &base).await?;
eprintln!("==> PR pipeline complete");
Ok(())
}
async fn run_main(client: &dagger_sdk::Query) -> eyre::Result<()> {
eprintln!("==> Main pipeline: check + test + build");
let base = build_base(client).await?;
eprintln!("--- cargo check --workspace");
base.clone()
.with_exec(vec!["cargo", "check", "--workspace"])
.sync()
.await?;
eprintln!("--- running tests");
run_tests(&base).await?;
eprintln!("--- building release image");
let image = build_release_image(client, &base).await?;
eprintln!("--- publishing image");
publish_image(client, &image).await?;
eprintln!("==> Main pipeline complete");
Ok(())
}
/// Load source from host, excluding build artifacts.
fn load_source(client: &dagger_sdk::Query) -> eyre::Result<dagger_sdk::Directory> {
let src = client.host().directory_opts(
".",
dagger_sdk::HostDirectoryOptsBuilder::default()
.exclude(vec!["target/", ".git/", "node_modules/", ".cuddle/"])
.build()?,
);
Ok(src)
}
/// Load dependency-only source (Cargo.toml + Cargo.lock, no src/ or tests/).
fn load_dep_source(client: &dagger_sdk::Query) -> eyre::Result<dagger_sdk::Directory> {
let src = client.host().directory_opts(
".",
dagger_sdk::HostDirectoryOptsBuilder::default()
.exclude(vec![
"target/",
".git/",
"node_modules/",
".cuddle/",
"**/src",
"**/tests",
])
.build()?,
);
Ok(src)
}
/// Create skeleton source files so cargo can resolve deps without real source.
fn create_skeleton_files(client: &dagger_sdk::Query) -> eyre::Result<dagger_sdk::Directory> {
let main_content = r#"fn main() { panic!("skeleton"); }"#;
let lib_content = r#"pub fn _skeleton() {}"#;
let crate_paths = discover_crates()?;
let mut dir = client.directory();
for crate_path in &crate_paths {
let src_dir = crate_path.join("src");
dir = dir.with_new_file(
src_dir.join("main.rs").to_string_lossy().to_string(),
main_content,
);
dir = dir.with_new_file(
src_dir.join("lib.rs").to_string_lossy().to_string(),
lib_content,
);
}
// Also add skeleton for ci/ crate itself.
dir = dir.with_new_file("ci/src/main.rs".to_string(), main_content);
Ok(dir)
}
/// Discover workspace crate directories on the host.
fn discover_crates() -> eyre::Result<Vec<PathBuf>> {
let mut crate_paths = Vec::new();
let crates_dir = PathBuf::from("crates");
if crates_dir.is_dir() {
for entry in std::fs::read_dir(&crates_dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
crate_paths.push(entry.path());
}
}
}
let examples_dir = PathBuf::from("examples");
if examples_dir.is_dir() {
for entry in std::fs::read_dir(&examples_dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
crate_paths.push(entry.path());
}
}
}
Ok(crate_paths)
}
/// Build the base Rust container with all deps cached.
async fn build_base(client: &dagger_sdk::Query) -> eyre::Result<dagger_sdk::Container> {
let src = load_source(client)?;
let dep_src = load_dep_source(client)?;
let skeleton = create_skeleton_files(client)?;
let dep_src_with_skeleton = dep_src.with_directory(".", skeleton);
// Base rust image with build tools (needs capnp compiler for sq-capnp-interface).
let rust_base = client
.container()
.from("rust:1.93-trixie")
.with_exec(vec!["apt", "update"])
.with_exec(vec!["apt", "install", "-y", "clang", "wget", "capnproto"])
// Install mold linker.
.with_exec(vec![
"wget",
"-q",
&format!(
"https://github.com/rui314/mold/releases/download/v{MOLD_VERSION}/mold-{MOLD_VERSION}-x86_64-linux.tar.gz"
),
])
.with_exec(vec![
"tar",
"-xf",
&format!("mold-{MOLD_VERSION}-x86_64-linux.tar.gz"),
])
.with_exec(vec![
"mv",
&format!("mold-{MOLD_VERSION}-x86_64-linux/bin/mold"),
"/usr/bin/mold",
]);
// Step 1: build deps with skeleton source (cacheable layer).
let prebuild = rust_base
.clone()
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src_with_skeleton)
.with_exec(vec!["cargo", "build", "--release", "--bin", BIN_NAME]);
// Step 2: copy cargo registry from prebuild (avoids re-downloading deps).
let build_container = rust_base
.with_workdir("/mnt/src")
.with_directory("/usr/local/cargo", prebuild.directory("/usr/local/cargo"))
.with_directory("/mnt/src/", src);
Ok(build_container)
}
/// Run tests (no external services needed — SQ tests are self-contained).
async fn run_tests(base: &dagger_sdk::Container) -> eyre::Result<()> {
base.clone()
.with_exec(vec!["cargo", "test", "--workspace"])
.sync()
.await?;
Ok(())
}
/// Build release binary and package into a slim image.
async fn build_release_image(
client: &dagger_sdk::Query,
base: &dagger_sdk::Container,
) -> eyre::Result<dagger_sdk::Container> {
let built = base
.clone()
.with_exec(vec!["cargo", "build", "--release", "--bin", BIN_NAME]);
let binary = built.file(format!("/mnt/src/target/release/{BIN_NAME}"));
// Distroless cc-debian13 matches the build image's glibc (trixie/2.38+)
// and includes libgcc + ca-certificates with no shell or package manager.
let final_image = client
.container()
.from("gcr.io/distroless/cc-debian13")
.with_file(format!("/usr/local/bin/{BIN_NAME}"), binary)
.with_exec(vec![BIN_NAME, "--help"]);
final_image.sync().await?;
// Set the final entrypoint for the published image.
let final_image = final_image.with_entrypoint(vec![BIN_NAME]);
eprintln!("--- release image built successfully");
Ok(final_image)
}
/// Publish image to container registry.
async fn publish_image(
client: &dagger_sdk::Query,
image: &dagger_sdk::Container,
) -> eyre::Result<()> {
let registry = std::env::var("CI_REGISTRY").unwrap_or_else(|_| "git.kjuulh.io".into());
let user = std::env::var("CI_REGISTRY_USER").unwrap_or_else(|_| "kjuulh".into());
let image_ref =
std::env::var("CI_IMAGE").unwrap_or_else(|_| format!("{registry}/{user}/sq:latest"));
let password = std::env::var("CI_REGISTRY_PASSWORD")
.map_err(|_| eyre::eyre!("CI_REGISTRY_PASSWORD must be set for publishing"))?;
image
.clone()
.with_registry_auth(
&registry,
&user,
client.set_secret("registry-password", &password),
)
.publish_opts(
&image_ref,
dagger_sdk::ContainerPublishOptsBuilder::default().build()?,
)
.await?;
eprintln!("--- published {image_ref}");
Ok(())
}