Move to multi crates project
ci/woodpecker/push/tests Pipeline was successful

Starting impl devcontainer spec
This commit is contained in:
qpismont committed 2026-07-31 20:06:37 +00:00
1 parent d711153553
commit 15f619ccf7
23 files changed
+271 -30

No files matched your search

+5 -6
View File
@@ -1,4 +1,4 @@
FROM debian:trixie FROM rust:1.97-trixie
ARG USERNAME=dev ARG USERNAME=dev
ARG USER_UID=1000 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/* && rm -rf /var/lib/apt/lists/*
RUN groupadd --gid ${USER_GID:-1000} $USERNAME \ 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 USER $USERNAME
WORKDIR /home/$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
+1
View File
@@ -19,5 +19,6 @@
}, },
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/herald,type=bind", "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/herald,type=bind",
"workspaceFolder": "/workspaces/herald", "workspaceFolder": "/workspaces/herald",
"runArgs": ["--userns=keep-id", "--security-opt", "label=disable"],
"appPort": [3000] "appPort": [3000]
} }
-4
View File
@@ -1,4 +0,0 @@
target/
.env
.devcontainer/
docs/
-3
View File
@@ -1,3 +0,0 @@
{
"rust-analyzer.check.command": "clippy"
}
+11
View File
@@ -0,0 +1,11 @@
{
"lsp": {
"rust-analyzer": {
"initialization_options": {
"check": {
"command": "clippy"
}
}
}
}
}
Generated
+15 -2
View File
@@ -490,6 +490,18 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "devcontainer-rs"
version = "0.1.0"
dependencies = [
"anyhow",
"serde",
"serde_json",
"tempfile",
"thiserror 2.0.18",
"tokio",
]
[[package]] [[package]]
name = "dispatch2" name = "dispatch2"
version = "0.3.1" version = "0.3.1"
@@ -787,12 +799,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]] [[package]]
name = "herald" name = "herald-server"
version = "1.1.0" version = "1.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
"bytes", "bytes",
"devcontainer-rs",
"dotenvy", "dotenvy",
"futures-util", "futures-util",
"hex", "hex",
+12 -10
View File
@@ -1,12 +1,11 @@
[package] [workspace]
name = "herald" members = [
version = "1.1.0" "crates/herald-server",
edition = "2024" "crates/devcontainer-rs",
]
resolver = "3"
[profile.release] [workspace.dependencies]
debug = 1
[dependencies]
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
tokio = { version = "1.53", features = ["full"] } tokio = { version = "1.53", features = ["full"] }
tokio-stream = "0.1" tokio-stream = "0.1"
@@ -19,9 +18,9 @@ sentry-anyhow = { version = "0.48", features = ["backtrace"] }
openrouter-rs = "0.12" openrouter-rs = "0.12"
dotenvy = "0.15" dotenvy = "0.15"
tower = "0.5" tower = "0.5"
tower-http = {version = "0.6", features = ["trace"] } tower-http = { version = "0.6", features = ["trace"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features=["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
axum = "0.8" axum = "0.8"
anyhow = { version = "1.0", features = ["backtrace"] } anyhow = { version = "1.0", features = ["backtrace"] }
thiserror = "2.0" thiserror = "2.0"
@@ -30,3 +29,6 @@ hex = "0.4"
bytes = "1.1" bytes = "1.1"
metrics = "0.24" metrics = "0.24"
metrics-exporter-prometheus = { version = "0.18", default-features = false, features = ["http-listener"] } metrics-exporter-prometheus = { version = "0.18", default-features = false, features = ["http-listener"] }
[profile.release]
debug = 1
+8 -5
View File
@@ -1,12 +1,15 @@
FROM rust:1.96 as builder FROM rust:1.97-trixie as builder
WORKDIR /app 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 FROM debian:trixie-slim
WORKDIR /app WORKDIR /app
COPY --from=builder /app/target/release/herald . COPY --from=builder /app/target/release/herald-server .
CMD [ "./herald" ] CMD [ "./herald-server" ]
+14
View File
@@ -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"
+175
View File
@@ -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<String>,
#[serde(default)]
pub args: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
pub struct DevContainerSchema {
#[serde(default)]
pub name: Option<String>,
pub build: DevContainerBuildSchema,
#[serde(rename = "workspaceFolder", default)]
pub workspace_folder: Option<String>,
#[serde(rename = "containerEnv", default)]
pub container_env: HashMap<String, String>,
#[serde(rename = "postCreateCommand", default)]
pub post_create_command: Option<String>,
#[serde(rename = "postStartCommand", default)]
pub post_start_command: Option<String>,
}
#[derive(Debug)]
pub struct DevContainer {
/// Absolute or relative path to the Dockerfile/Containerfile to build.
pub container_file_path: PathBuf,
pub name: Option<String>,
pub build_args: HashMap<String, String>,
pub container_env: HashMap<String, String>,
pub workspace_folder: Option<String>,
pub post_create_command: Option<String>,
pub post_start_command: Option<String>,
}
#[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<Self, Self::Error> {
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<Path>) -> Result<DevContainer, ParseError> {
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::<DevContainerSchema>(&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);
}
}
+30
View File
@@ -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" }
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.