Watch
0
0
Fork
You've already forked hyperhive
0

hive-c0re: a refused rev is an error log + Grafana alert, not a PR comment

A deploy request whose rev hive-c0re refuses (fetch failed, or the rev
does not descend from applied/main) no longer tries to comment on the PR
that merged it. Off the forge host that attempt always failed with "forge
core token absent". The refusal now logs at ERROR, message unchanged, and
the new `hh-config-rev-refused` Grafana rule fires on it.

Deleted with the posting: actions::post_rev_deploy_failure_to_pr, the
whole forge/pr_comment.rs (config_repo, merged_pr_for_commit,
post_pr_comment), and ReaderToken::value, whose only caller was
merged_pr_for_commit.

Refs #4851
This commit is contained in:
atlas 2026-10-09 21:53:17 +02:00 • committed by mara
commit 8c222f8910
9 changed files with 27 additions and 106 deletions

View file

@ -5,13 +5,11 @@
//! `internal/docs`, are swarm-controller's. Full design:
//! `docs/integrations/forge.md`.
mod pr_comment;
mod reader;
mod reconcile;
mod repos;
mod users;
pub use pr_comment::{config_repo, merged_pr_for_commit, post_pr_comment};
pub use reader::reader_token;
pub use reconcile::{fetch_forge_main, reconcile_config_apply, reconcile_config_status};
pub use repos::{
@ -462,13 +460,13 @@ mod tests {
async fn a_request_the_forge_never_answers_errors_at_the_deadline() {
let started = tokio::time::Instant::now();
let err = bounded(
"post PR comment",
"pending request",
std::future::pending::<Result<(), forgejo_api::ForgejoError>>(),
)
.await
.expect_err("a pending request must hit the deadline");
assert_eq!(started.elapsed(), API_TIMEOUT);
assert!(err.to_string().contains("post PR comment"), "{err}");
assert!(err.to_string().contains("pending request"), "{err}");
}
/// The request's own result passes through for the caller to classify.

View file

@ -1,60 +0,0 @@
//! Commenting on an agent's config PRs as the core forge user — how a hive
//! reports a merged config commit it refused to deploy back onto the PR that
//! merged it. Finding that PR only reads, so it uses [`reader_token`].
use anyhow::{Context, Result};
use super::{CONFIG_ORG, api, bounded, core_token, reader_token};
/// Full `owner/name` path of an agent's config repo on the forge.
pub fn config_repo(agent: &str) -> String {
format!("{CONFIG_ORG}/{agent}")
}
/// The number of the merged PR that put commit `sha` on `repo`'s base branch.
///
/// # Errors
/// No read token, malformed repo, no such PR, or transport/API failure.
pub async fn merged_pr_for_commit(repo: &str, sha: &str) -> Result<u64> {
let reader = reader_token().await.context("no forge read token")?;
let (owner, name) = repo
.split_once('/')
.with_context(|| format!("forge repo `{repo}` is not owner/name"))?;
let pull = bounded(
"GET pull request of commit",
api(reader.value())?.repo_get_commit_pull_request(owner, name, sha),
)
.await?
.context("GET pull request of commit")?;
pull.number
.and_then(|n| u64::try_from(n).ok())
.with_context(|| format!("pull request of {sha} in {repo} carries no number"))
}
/// Post a comment to PR (= issue) `pr` on `repo` as the core forge user.
/// PRs are issues in Forgejo, so the PR number is the issue index.
///
/// # Errors
/// Absent core token, malformed repo, or transport/API failure.
pub async fn post_pr_comment(repo: &str, pr: u64, body: &str) -> Result<()> {
let token = core_token().context("forge core token absent")?;
let (owner, name) = repo
.split_once('/')
.with_context(|| format!("forge repo `{repo}` is not owner/name"))?;
let index = i64::try_from(pr).with_context(|| format!("PR index {pr} overflows i64"))?;
bounded(
"post PR comment",
api(&token)?.issue_create_comment(
owner,
name,
index,
forgejo_api::structs::CreateIssueCommentOption {
body: body.to_owned(),
updated_at: None,
},
),
)
.await?
.context("post PR comment")?;
Ok(())
}

View file

@ -30,11 +30,6 @@ impl std::fmt::Debug for ReaderToken {
}
impl ReaderToken {
/// The token itself, for an API client.
pub fn value(&self) -> &str {
&self.value
}
/// The `http.extraHeader` value for git; see [`basic_auth_header`].
pub fn auth_header(&self) -> String {
basic_auth_header(&self.login, &self.value)
@ -121,14 +116,14 @@ mod tests {
#[test]
fn the_store_wins_over_the_core_file() {
let t = pick(Some(stored()), Some("from-the-file".to_owned())).expect("a token");
assert_eq!(t.value(), "from-the-store");
assert_eq!(t.value, "from-the-store");
assert_eq!(t.login, "hive-alpha");
}
#[test]
fn the_core_file_is_the_fallback() {
let t = pick(None, Some("from-the-file".to_owned())).expect("a token");
assert_eq!(t.value(), "from-the-file");
assert_eq!(t.value, "from-the-file");
assert_eq!(t.login, CORE_LOGIN);
}

View file

@ -256,7 +256,7 @@ async fn check_core_token(token: &str) -> CoreTokenCheck {
/// Ensure the bootstrap `core` admin user + a token at
/// `CORE_TOKEN_PATH`. The token is what hive-c0re uses for forgejo
/// API calls (meta-repo push, collaborator grants, PR comments).
/// API calls (meta-repo push, collaborator grants).
/// Returns the token.
///
/// Idempotent, but validity-aware: when a token file is already present