Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"crates/config",
"crates/github",
"crates/store",
"crates/webhook",
"crates/worker",
"crates/server",
Expand Down Expand Up @@ -37,3 +38,4 @@ uuid = { version = "1", features = ["v4"] }
hex = "0.4"
toml = "0.8"
wiremock = "0.6"
rusqlite = { version = "0.32", features = ["bundled"] }
6 changes: 6 additions & 0 deletions config/example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ workspace_root = "/tmp/coven-github-tasks" # Ephemeral workspace root
timeout_secs = 600 # 10 minutes per task
max_retries = 2 # Retries for infra errors (exit code 2)

[storage]
# Durable adapter state (SQLite): webhook-delivery idempotency and the task
# queue. Mount its directory on a persistent volume so state survives restarts
# (issue #2). The parent directory is created on first run.
path = "data/coven-github.db"

# ── Familiar configuration ──────────────────────────────────────────────────
# Add one [[familiars]] block per familiar you want to expose as a GitHub bot.

Expand Down
97 changes: 97 additions & 0 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,31 @@ pub struct Config {
/// Automatic review trigger policy. Absent section = all lanes off.
#[serde(default)]
pub review: ReviewConfig,
/// Durable state (SQLite) location. Absent section = the default path.
#[serde(default)]
pub storage: StorageConfig,
}

/// Durable task-store configuration (issue #2).
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct StorageConfig {
/// Path to the SQLite database file for durable adapter state. Its parent
/// directory is created on first run; mount it on a persistent volume so
/// task state survives restarts.
#[serde(default = "default_storage_path")]
pub path: PathBuf,
}

impl Default for StorageConfig {
fn default() -> Self {
StorageConfig {
path: default_storage_path(),
}
}
}

fn default_storage_path() -> PathBuf {
PathBuf::from("data/coven-github.db")
}

/// Automatic review trigger policy (issue #10). Lanes default to off.
Expand Down Expand Up @@ -290,6 +315,32 @@ impl Config {
}
}

// ── Durable storage ─────────────────────────────────────────────
// The DB file itself is created on first run; only the parent dir
// matters here. A parent that exists as a non-directory is fatal; a
// missing parent is fine (Store::open creates it).
match self.storage.path.parent() {
Some(parent) if parent.as_os_str().is_empty() => {}
Some(parent) => match std::fs::metadata(parent) {
Ok(meta) if meta.is_dir() => {}
Ok(_) => out.push(Diagnostic::error(
"storage.path",
format!(
"storage directory '{}' exists but is not a directory.",
parent.display()
),
)),
Err(_) => out.push(Diagnostic::warning(
"storage.path",
format!(
"storage directory '{}' does not exist yet — it will be created on first run.",
parent.display()
),
)),
},
Comment on lines +333 to +340
None => {}
}

out
}
}
Expand Down Expand Up @@ -370,6 +421,9 @@ fn next_step_for(field: &str, _message: &str) -> &'static str {
"review.familiar" => {
"Set review.familiar (or the repo override's familiar) to the id of a configured [[familiars]] block."
}
"storage.path" => {
"Point storage.path at a file on a writable, persistent volume (its parent directory is created on first run)."
}
_ => "Update this config field, then rerun coven-github doctor.",
}
}
Expand Down Expand Up @@ -468,6 +522,7 @@ mod tests {
worker,
familiars,
review: ReviewConfig::default(),
storage: StorageConfig::default(),
}
}

Expand Down Expand Up @@ -534,6 +589,48 @@ mod tests {
);
}

#[test]
fn storage_defaults_to_the_data_dir_path() {
assert_eq!(
StorageConfig::default().path,
PathBuf::from("data/coven-github.db")
);
}

#[test]
fn doctor_flags_storage_parent_that_is_a_file() {
let dir = tmpdir();
let pem = write_pem(&dir);
let bin = write_bin(&dir);
// Make a plain file, then point the DB path *inside* it — its parent is
// a file, which can never hold a database.
let not_a_dir = dir.join("blocker");
std::fs::write(&not_a_dir, b"x").unwrap();
let mut cfg = config_with(
GitHubAppConfig {
app_id: 123,
private_key_path: pem,
webhook_secret: "a-long-random-webhook-secret".into(),
api_base_url: None,
},
WorkerConfig {
concurrency: 4,
coven_code_bin: bin,
workspace_root: dir.clone(),
timeout_secs: 600,
max_retries: 2,
},
vec![good_familiar()],
);
cfg.storage.path = not_a_dir.join("coven-github.db");

assert!(
errors(&cfg.check()).contains(&"storage.path"),
"diags: {:?}",
cfg.check()
);
}

#[test]
fn review_policy_defaults_to_disabled() {
let policy = ReviewConfig::default();
Expand Down
1 change: 1 addition & 0 deletions crates/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ tracing.workspace = true
tracing-subscriber.workspace = true
coven-github-api = { path = "../github" }
coven-github-config = { path = "../config" }
coven-github-store = { path = "../store" }
coven-github-webhook = { path = "../webhook" }
coven-github-worker = { path = "../worker" }
6 changes: 6 additions & 0 deletions crates/server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ async fn main() -> Result<()> {
);
}

// Open durable state before serving so a bad storage path fails
// fast rather than on the first webhook (issue #2).
let store = coven_github_store::Store::open(&config.storage.path)?;
tracing::info!(path = %config.storage.path.display(), "durable store ready");

let config = Arc::new(config);

let (task_tx, task_rx) = mpsc::channel(256);
Expand All @@ -101,6 +106,7 @@ async fn main() -> Result<()> {
config: config.clone(),
task_tx,
task_store,
store,
};

let app = Router::new()
Expand Down
17 changes: 17 additions & 0 deletions crates/store/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "coven-github-store"
version.workspace = true
edition.workspace = true
license.workspace = true

[dependencies]
anyhow.workspace = true
chrono.workspace = true
rusqlite.workspace = true
serde_json.workspace = true
tokio.workspace = true
tracing.workspace = true
coven-github-api = { path = "../github" }

[dev-dependencies]
uuid.workspace = true
Loading