Compare commits
4 changed files with 10 additions and 95 deletions
|
|
@ -50,24 +50,6 @@ hive-c0re maintains the local clone at
|
||||||
restarted between pushes). The pull is best-effort — a failure
|
restarted between pushes). The pull is best-effort — a failure
|
||||||
logs a warning and does not affect the rest of the daemon.
|
logs a warning and does not affect the rest of the daemon.
|
||||||
|
|
||||||
Both paths share the same `knowledge::pull()` function, which also
|
|
||||||
handles the change notice below — neither path can forget to wire it
|
|
||||||
in since the broadcast logic lives once, in `pull()` itself, not at
|
|
||||||
each call site.
|
|
||||||
|
|
||||||
### Change notice
|
|
||||||
|
|
||||||
When a pull actually moves the local clone's `HEAD` (a real change,
|
|
||||||
not a no-op — e.g. the periodic pull finding nothing new), hive-c0re
|
|
||||||
broadcasts a short notice to every currently-registered agent's inbox:
|
|
||||||
sender `system`, body `[system] /knowledge updated:` followed by a
|
|
||||||
`git diff --stat <old>..<new>` summary of what changed (or a generic
|
|
||||||
"see the repo" fallback if computing the diff itself fails). This is
|
|
||||||
the same broadcast mechanism used for other hive-wide notices — inbox
|
|
||||||
message only, no forced wake, and it carries the standard "this was a
|
|
||||||
broadcast" hint. A diff or per-agent send failure is logged but never
|
|
||||||
blocks the pull itself.
|
|
||||||
|
|
||||||
The local clone is created (or refreshed) once at startup via
|
The local clone is created (or refreshed) once at startup via
|
||||||
`knowledge::ensure_local_clone`. If the repo is brand new and
|
`knowledge::ensure_local_clone`. If the repo is brand new and
|
||||||
empty, `ensure_local_clone` seeds it with the default README
|
empty, `ensure_local_clone` seeds it with the default README
|
||||||
|
|
|
||||||
|
|
@ -129,9 +129,8 @@ pub(super) async fn post_webhook_knowledge(
|
||||||
return (StatusCode::OK, "ignored").into_response();
|
return (StatusCode::OK, "ignored").into_response();
|
||||||
}
|
}
|
||||||
tracing::info!("webhook/knowledge: pull triggered by push to {expected_repo}");
|
tracing::info!("webhook/knowledge: pull triggered by push to {expected_repo}");
|
||||||
let coord = state.coord.clone();
|
tokio::spawn(async {
|
||||||
tokio::spawn(async move {
|
if let Err(e) = crate::knowledge::pull().await {
|
||||||
if let Err(e) = crate::knowledge::pull(&coord).await {
|
|
||||||
tracing::warn!(error = ?e, "webhook/knowledge: pull failed");
|
tracing::warn!(error = ?e, "webhook/knowledge: pull failed");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -376,12 +376,11 @@ async fn cmd_serve(
|
||||||
// missed (e.g. hive-c0re was down during a push). First fires at
|
// missed (e.g. hive-c0re was down during a push). First fires at
|
||||||
// startup (immediate pull after the clone is already present).
|
// startup (immediate pull after the clone is already present).
|
||||||
let mut knowledge_shutdown = coord.shutdown_rx();
|
let mut knowledge_shutdown = coord.shutdown_rx();
|
||||||
let knowledge_coord = coord.clone();
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
// Initial pull — reconcile any commits that landed while c0re
|
// Initial pull — reconcile any commits that landed while c0re
|
||||||
// was offline. Not fed to the health tracker: a startup miss is
|
// was offline. Not fed to the health tracker: a startup miss is
|
||||||
// expected (the clone may not exist yet) and is logged at debug.
|
// expected (the clone may not exist yet) and is logged at debug.
|
||||||
if let Err(e) = knowledge::pull(&knowledge_coord).await {
|
if let Err(e) = knowledge::pull().await {
|
||||||
tracing::debug!(error = ?e, "knowledge: startup pull skipped (no clone yet?)");
|
tracing::debug!(error = ?e, "knowledge: startup pull skipped (no clone yet?)");
|
||||||
}
|
}
|
||||||
// Persistent-failure → banner. An hourly sweep that keeps failing for
|
// Persistent-failure → banner. An hourly sweep that keeps failing for
|
||||||
|
|
@ -394,7 +393,7 @@ async fn cmd_serve(
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
() = tokio::time::sleep(interval) => {
|
() = tokio::time::sleep(interval) => {
|
||||||
match knowledge::pull(&knowledge_coord).await {
|
match knowledge::pull().await {
|
||||||
Ok(()) => health.record_ok(),
|
Ok(()) => health.record_ok(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(error = ?e, "knowledge: periodic pull failed");
|
tracing::warn!(error = ?e, "knowledge: periodic pull failed");
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ use std::collections::BTreeMap;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use forgejo_api::structs::{CreateHookOption, CreateHookOptionConfig, CreateHookOptionType};
|
use forgejo_api::structs::{CreateHookOption, CreateHookOptionConfig, CreateHookOptionType};
|
||||||
|
|
||||||
use crate::coordinator::Coordinator;
|
|
||||||
use crate::forge::forge_git_url;
|
use crate::forge::forge_git_url;
|
||||||
|
|
||||||
pub const ORG: &str = "internal";
|
pub const ORG: &str = "internal";
|
||||||
|
|
@ -242,92 +241,28 @@ pub async fn ensure_webhook(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Current `HEAD` sha of the local clone, best-effort. `None` on any
|
|
||||||
/// failure (used only to detect whether a pull actually moved `HEAD` —
|
|
||||||
/// worth skipping the broadcast over, not worth failing the pull for).
|
|
||||||
async fn head_sha() -> Option<String> {
|
|
||||||
let out = tokio::process::Command::new("git")
|
|
||||||
.args(["-C", LOCAL_DIR, "rev-parse", "HEAD"])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.ok()?;
|
|
||||||
out.status
|
|
||||||
.success()
|
|
||||||
.then(|| String::from_utf8_lossy(&out.stdout).trim().to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pull the latest changes in the local clone. Called from the webhook
|
/// Pull the latest changes in the local clone. Called from the webhook
|
||||||
/// handler on every push to `internal/knowledge` main, and periodically
|
/// handler on every push to `internal/knowledge` main, and periodically
|
||||||
/// from `main.rs` as a fallback. Uses `--ff-only` so a force-push to
|
/// from `main.rs` as a fallback. Uses `--ff-only` so a force-push to
|
||||||
/// the knowledge repo never wedges the local copy silently.
|
/// the knowledge repo never wedges the local copy silently.
|
||||||
///
|
pub async fn pull() -> Result<()> {
|
||||||
/// When the pull actually moves `HEAD` (a real change, not a no-op),
|
|
||||||
/// broadcasts a short `git diff --stat` summary to every live agent's
|
|
||||||
/// inbox via `coord` — inbox-only, no forced wake, and shared by both
|
|
||||||
/// call sites since the broadcast lives in here rather than in each
|
|
||||||
/// caller.
|
|
||||||
pub async fn pull(coord: &Coordinator) -> Result<()> {
|
|
||||||
// Sanity: if the clone is missing (e.g. storage was wiped), refuse
|
// Sanity: if the clone is missing (e.g. storage was wiped), refuse
|
||||||
// to pull and let the caller decide whether to re-clone.
|
// to pull and let the caller decide whether to re-clone.
|
||||||
let git_dir = std::path::Path::new(LOCAL_DIR).join(".git");
|
let git_dir = std::path::Path::new(LOCAL_DIR).join(".git");
|
||||||
if !git_dir.exists() {
|
if !git_dir.exists() {
|
||||||
anyhow::bail!("knowledge: {LOCAL_DIR}/.git not found — clone first");
|
anyhow::bail!("knowledge: {LOCAL_DIR}/.git not found — clone first");
|
||||||
}
|
}
|
||||||
let before = head_sha().await;
|
|
||||||
let out = tokio::process::Command::new("git")
|
let out = tokio::process::Command::new("git")
|
||||||
.args(["-C", LOCAL_DIR, "pull", "--ff-only"])
|
.args(["-C", LOCAL_DIR, "pull", "--ff-only"])
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.context("git pull knowledge")?;
|
.context("git pull knowledge")?;
|
||||||
if !out.status.success() {
|
if out.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_owned();
|
||||||
|
tracing::info!(result = %stdout, "knowledge: pull succeeded");
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned();
|
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned();
|
||||||
anyhow::bail!("git pull {ORG}/{REPO} failed: {stderr}")
|
anyhow::bail!("git pull {ORG}/{REPO} failed: {stderr}")
|
||||||
}
|
}
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_owned();
|
|
||||||
tracing::info!(result = %stdout, "knowledge: pull succeeded");
|
|
||||||
|
|
||||||
let after = head_sha().await;
|
|
||||||
if let (Some(before), Some(after)) = (before, after)
|
|
||||||
&& before != after
|
|
||||||
{
|
|
||||||
broadcast_change(coord, &before, &after).await;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Diff `before..after` in the local clone and broadcast a one-line-per-file
|
|
||||||
/// summary to every live agent. Best-effort — a diff or send failure is
|
|
||||||
/// logged, never propagated (the pull itself already succeeded).
|
|
||||||
async fn broadcast_change(coord: &Coordinator, before: &str, after: &str) {
|
|
||||||
let diff = tokio::process::Command::new("git")
|
|
||||||
.args([
|
|
||||||
"-C",
|
|
||||||
LOCAL_DIR,
|
|
||||||
"diff",
|
|
||||||
"--stat",
|
|
||||||
&format!("{before}..{after}"),
|
|
||||||
])
|
|
||||||
.output()
|
|
||||||
.await;
|
|
||||||
let stat = match diff {
|
|
||||||
Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout).trim().to_owned(),
|
|
||||||
Ok(out) => {
|
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned();
|
|
||||||
tracing::warn!(%stderr, "knowledge: diff --stat failed; broadcasting without it");
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(error = ?e, "knowledge: diff --stat failed; broadcasting without it");
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let body = if stat.is_empty() {
|
|
||||||
"[system] /knowledge updated — see the repo for what changed.".to_owned()
|
|
||||||
} else {
|
|
||||||
format!("[system] /knowledge updated:\n{stat}")
|
|
||||||
};
|
|
||||||
let errors = coord.broadcast_send(hive_sh4re::SYSTEM_SENDER, &body);
|
|
||||||
if !errors.is_empty() {
|
|
||||||
tracing::warn!(?errors, "knowledge: broadcast had per-agent failures");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue