From 15f619ccf76768c152eb0c495fdfb4efd3b374a0 Mon Sep 17 00:00:00 2001 From: qpismont Date: Fri, 31 Jul 2026 20:06:37 +0000 Subject: [PATCH] Move to multi crates project Starting impl devcontainer spec --- .devcontainer/Dockerfile | 11 +- .devcontainer/devcontainer.json | 1 + .dockerignore | 4 - .vscode/settings.json | 3 - .zed/settings.json | 11 ++ Cargo.lock | 17 +- Cargo.toml | 22 ++- Containerfile | 13 +- crates/devcontainer-rs/Cargo.toml | 14 ++ crates/devcontainer-rs/src/lib.rs | 175 ++++++++++++++++++ crates/herald-server/Cargo.toml | 30 +++ {src => crates/herald-server/src}/api.rs | 0 {src => crates/herald-server/src}/bot.rs | 0 .../herald-server/src}/bot_actions/mod.rs | 0 .../herald-server/src}/bot_actions/review.rs | 0 {src => crates/herald-server/src}/consts.rs | 0 {src => crates/herald-server/src}/env.rs | 0 {src => crates/herald-server/src}/errors.rs | 0 {src => crates/herald-server/src}/gitea.rs | 0 {src => crates/herald-server/src}/main.rs | 0 {src => crates/herald-server/src}/metrics.rs | 0 .../herald-server/src}/open_router.rs | 0 {src => crates/herald-server/src}/state.rs | 0 23 files changed, 271 insertions(+), 30 deletions(-) delete mode 100644 .dockerignore delete mode 100644 .vscode/settings.json create mode 100644 .zed/settings.json create mode 100644 crates/devcontainer-rs/Cargo.toml create mode 100644 crates/devcontainer-rs/src/lib.rs create mode 100644 crates/herald-server/Cargo.toml rename {src => crates/herald-server/src}/api.rs (100%) rename {src => crates/herald-server/src}/bot.rs (100%) rename {src => crates/herald-server/src}/bot_actions/mod.rs (100%) rename {src => crates/herald-server/src}/bot_actions/review.rs (100%) rename {src => crates/herald-server/src}/consts.rs (100%) rename {src => crates/herald-server/src}/env.rs (100%) rename {src => crates/herald-server/src}/errors.rs (100%) rename {src => crates/herald-server/src}/gitea.rs (100%) rename {src => crates/herald-server/src}/main.rs (100%) rename {src => crates/herald-server/src}/metrics.rs (100%) rename {src => crates/herald-server/src}/open_router.rs (100%) rename {src => crates/herald-server/src}/state.rs (100%) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1b89658..dd02efa 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:trixie +FROM rust:1.97-trixie ARG USERNAME=dev ARG USER_UID=1000 @@ -18,11 +18,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* RUN groupadd --gid ${USER_GID:-1000} $USERNAME \ - && useradd --uid ${USER_UID:-1000} --gid ${USER_GID:-1000} -m $USERNAME + && useradd --uid ${USER_UID:-1000} --gid ${USER_GID:-1000} -m $USERNAME \ + && rustup component add clippy + + USER $USERNAME WORKDIR /home/$USERNAME - -ENV PATH="/home/${USERNAME}/.cargo/bin:${PATH}" - -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a0fc52a..b9310d6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -19,5 +19,6 @@ }, "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/herald,type=bind", "workspaceFolder": "/workspaces/herald", + "runArgs": ["--userns=keep-id", "--security-opt", "label=disable"], "appPort": [3000] } diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 89343aa..0000000 --- a/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -target/ -.env -.devcontainer/ -docs/ diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 660eb93..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rust-analyzer.check.command": "clippy" -} \ No newline at end of file diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..d569920 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,11 @@ +{ + "lsp": { + "rust-analyzer": { + "initialization_options": { + "check": { + "command": "clippy" + } + } + } + } +} diff --git a/Cargo.lock b/Cargo.lock index 7f4d25b..3def019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -490,6 +490,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "devcontainer-rs" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "dispatch2" version = "0.3.1" @@ -787,12 +799,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] -name = "herald" -version = "1.1.0" +name = "herald-server" +version = "1.2.0" dependencies = [ "anyhow", "axum", "bytes", + "devcontainer-rs", "dotenvy", "futures-util", "hex", diff --git a/Cargo.toml b/Cargo.toml index 3340e25..0365198 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,11 @@ -[package] -name = "herald" -version = "1.1.0" -edition = "2024" +[workspace] +members = [ + "crates/herald-server", + "crates/devcontainer-rs", +] +resolver = "3" -[profile.release] -debug = 1 - -[dependencies] +[workspace.dependencies] reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } tokio = { version = "1.53", features = ["full"] } tokio-stream = "0.1" @@ -19,9 +18,9 @@ sentry-anyhow = { version = "0.48", features = ["backtrace"] } openrouter-rs = "0.12" dotenvy = "0.15" tower = "0.5" -tower-http = {version = "0.6", features = ["trace"] } +tower-http = { version = "0.6", features = ["trace"] } tracing = "0.1" -tracing-subscriber = { version = "0.3", features=["env-filter"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } axum = "0.8" anyhow = { version = "1.0", features = ["backtrace"] } thiserror = "2.0" @@ -30,3 +29,6 @@ hex = "0.4" bytes = "1.1" metrics = "0.24" metrics-exporter-prometheus = { version = "0.18", default-features = false, features = ["http-listener"] } + +[profile.release] +debug = 1 diff --git a/Containerfile b/Containerfile index 6508b0c..7150f27 100644 --- a/Containerfile +++ b/Containerfile @@ -1,12 +1,15 @@ -FROM rust:1.96 as builder +FROM rust:1.97-trixie as builder WORKDIR /app -COPY . . -RUN cargo build --release + +COPY Cargo.toml Cargo.lock ./ +COPY crates/ crates/ + +RUN cargo build --release --package herald-server FROM debian:trixie-slim WORKDIR /app -COPY --from=builder /app/target/release/herald . -CMD [ "./herald" ] +COPY --from=builder /app/target/release/herald-server . +CMD [ "./herald-server" ] diff --git a/crates/devcontainer-rs/Cargo.toml b/crates/devcontainer-rs/Cargo.toml new file mode 100644 index 0000000..1dd07fb --- /dev/null +++ b/crates/devcontainer-rs/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "devcontainer-rs" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +tempfile = "3" diff --git a/crates/devcontainer-rs/src/lib.rs b/crates/devcontainer-rs/src/lib.rs new file mode 100644 index 0000000..44c8bc3 --- /dev/null +++ b/crates/devcontainer-rs/src/lib.rs @@ -0,0 +1,175 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct DevContainerBuildSchema { + #[serde(default)] + pub dockerfile: Option, + #[serde(default)] + pub args: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct DevContainerSchema { + #[serde(default)] + pub name: Option, + pub build: DevContainerBuildSchema, + + #[serde(rename = "workspaceFolder", default)] + pub workspace_folder: Option, + + #[serde(rename = "containerEnv", default)] + pub container_env: HashMap, + + #[serde(rename = "postCreateCommand", default)] + pub post_create_command: Option, + + #[serde(rename = "postStartCommand", default)] + pub post_start_command: Option, +} + +#[derive(Debug)] +pub struct DevContainer { + /// Absolute or relative path to the Dockerfile/Containerfile to build. + pub container_file_path: PathBuf, + pub name: Option, + pub build_args: HashMap, + pub container_env: HashMap, + pub workspace_folder: Option, + pub post_create_command: Option, + pub post_start_command: Option, +} + +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + #[error("failed to read devcontainer file `{path}`: {source}")] + Read { + path: PathBuf, + source: std::io::Error, + }, + + #[error("invalid devcontainer JSON in `{path}`: {source}")] + Json { + path: PathBuf, + source: serde_json::Error, + }, + + #[error("container file `{0}` does not exist or is not a regular file")] + ContainerFileNotFound(PathBuf), + + #[error("the devcontainer file path has no parent directory: `{0}`")] + InvalidDevContainerPath(PathBuf), +} + +impl TryFrom<(DevContainerSchema, PathBuf)> for DevContainer { + type Error = ParseError; + + fn try_from((schema, devcontainer_path): (DevContainerSchema, PathBuf)) -> Result { + let base_dir = devcontainer_path + .parent() + .ok_or_else(|| ParseError::InvalidDevContainerPath(devcontainer_path.clone()))?; + + let container_file_path = match schema.build.dockerfile.as_deref() { + Some(file) => base_dir.join(file), + None => first_existing_container_file(base_dir), + }; + + if !container_file_path.is_file() { + return Err(ParseError::ContainerFileNotFound(container_file_path)); + } + + Ok(Self { + container_file_path, + name: schema.name, + build_args: schema.build.args, + container_env: schema.container_env, + workspace_folder: schema.workspace_folder, + post_create_command: schema.post_create_command, + post_start_command: schema.post_start_command, + }) + } +} + +fn first_existing_container_file(base_dir: &Path) -> PathBuf { + ["Dockerfile", "Containerfile"] + .iter() + .map(|filename| base_dir.join(filename)) + .find(|path| path.is_file()) + .unwrap_or_else(|| base_dir.join("Dockerfile")) +} + +pub async fn parse(path: impl AsRef) -> Result { + let path = path.as_ref().to_path_buf(); + let contents = tokio::fs::read_to_string(&path) + .await + .map_err(|source| ParseError::Read { + path: path.clone(), + source, + })?; + + let schema = serde_json::from_str::(&contents).map_err(|source| { + ParseError::Json { + path: path.clone(), + source, + } + })?; + + DevContainer::try_from((schema, path)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn resolves_configured_containerfile_relative_to_devcontainer_file() { + let dir = tempfile::tempdir().unwrap(); + let devcontainer_path = dir.path().join("devcontainer.json"); + let containerfile_path = dir.path().join("Containerfile"); + fs::write(&containerfile_path, "FROM alpine\n").unwrap(); + + let schema = DevContainerSchema { + name: Some("test".into()), + build: DevContainerBuildSchema { + dockerfile: Some("Containerfile".into()), + args: HashMap::new(), + }, + workspace_folder: None, + container_env: HashMap::new(), + post_create_command: None, + post_start_command: None, + }; + + let config = DevContainer::try_from((schema, devcontainer_path)).unwrap(); + assert_eq!(config.container_file_path, containerfile_path); + } + + #[test] + fn falls_back_to_dockerfile_before_containerfile() { + let dir = tempfile::tempdir().unwrap(); + let devcontainer_path = dir.path().join("devcontainer.json"); + let dockerfile_path = dir.path().join("Dockerfile"); + fs::write(&dockerfile_path, "FROM alpine\n").unwrap(); + fs::write(dir.path().join("Containerfile"), "FROM busybox\n").unwrap(); + + let schema = DevContainerSchema { + name: None, + build: DevContainerBuildSchema { + dockerfile: None, + args: HashMap::new(), + }, + workspace_folder: None, + container_env: HashMap::new(), + post_create_command: None, + post_start_command: None, + }; + + let config = DevContainer::try_from((schema, devcontainer_path)).unwrap(); + assert_eq!(config.container_file_path, dockerfile_path); + } +} diff --git a/crates/herald-server/Cargo.toml b/crates/herald-server/Cargo.toml new file mode 100644 index 0000000..8e015d9 --- /dev/null +++ b/crates/herald-server/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "herald-server" +version = "1.2.0" +edition = "2024" + +[dependencies] +reqwest = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +tokio-util = { workspace = true } +futures-util = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true } +sentry = { workspace = true } +sentry-anyhow = { workspace = true } +openrouter-rs = { workspace = true } +dotenvy = { workspace = true } +tower = { workspace = true } +tower-http = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +axum = { workspace = true } +anyhow = { workspace = true } +thiserror = { workspace = true } +ring = { workspace = true } +hex = { workspace = true } +bytes = { workspace = true } +metrics = { workspace = true } +metrics-exporter-prometheus = { workspace = true } +devcontainer-rs = { path = "../devcontainer-rs" } \ No newline at end of file diff --git a/src/api.rs b/crates/herald-server/src/api.rs similarity index 100% rename from src/api.rs rename to crates/herald-server/src/api.rs diff --git a/src/bot.rs b/crates/herald-server/src/bot.rs similarity index 100% rename from src/bot.rs rename to crates/herald-server/src/bot.rs diff --git a/src/bot_actions/mod.rs b/crates/herald-server/src/bot_actions/mod.rs similarity index 100% rename from src/bot_actions/mod.rs rename to crates/herald-server/src/bot_actions/mod.rs diff --git a/src/bot_actions/review.rs b/crates/herald-server/src/bot_actions/review.rs similarity index 100% rename from src/bot_actions/review.rs rename to crates/herald-server/src/bot_actions/review.rs diff --git a/src/consts.rs b/crates/herald-server/src/consts.rs similarity index 100% rename from src/consts.rs rename to crates/herald-server/src/consts.rs diff --git a/src/env.rs b/crates/herald-server/src/env.rs similarity index 100% rename from src/env.rs rename to crates/herald-server/src/env.rs diff --git a/src/errors.rs b/crates/herald-server/src/errors.rs similarity index 100% rename from src/errors.rs rename to crates/herald-server/src/errors.rs diff --git a/src/gitea.rs b/crates/herald-server/src/gitea.rs similarity index 100% rename from src/gitea.rs rename to crates/herald-server/src/gitea.rs diff --git a/src/main.rs b/crates/herald-server/src/main.rs similarity index 100% rename from src/main.rs rename to crates/herald-server/src/main.rs diff --git a/src/metrics.rs b/crates/herald-server/src/metrics.rs similarity index 100% rename from src/metrics.rs rename to crates/herald-server/src/metrics.rs diff --git a/src/open_router.rs b/crates/herald-server/src/open_router.rs similarity index 100% rename from src/open_router.rs rename to crates/herald-server/src/open_router.rs diff --git a/src/state.rs b/crates/herald-server/src/state.rs similarity index 100% rename from src/state.rs rename to crates/herald-server/src/state.rs